Index
Main Lutris package
__version__
special
¶
api
¶
Functions to interact with the Lutris REST API
API_KEY_FILE_PATH
¶
USER_ICON_FILE_PATH
¶
USER_INFO_FILE_PATH
¶
connect(username, password)
¶
Connect to the Lutris API
Source code in lutris/api.py
def connect(username, password):
"""Connect to the Lutris API"""
login_url = settings.SITE_URL + "/api/accounts/token"
credentials = {"username": username, "password": password}
try:
response = requests.post(url=login_url, data=credentials, timeout=10)
response.raise_for_status()
json_dict = response.json()
if "token" in json_dict:
token = json_dict["token"]
with open(API_KEY_FILE_PATH, "w", encoding='utf-8') as token_file:
token_file.write("%s:%s" % (username, token))
get_user_info()
return token
except (requests.RequestException, requests.ConnectionError, requests.HTTPError, requests.TooManyRedirects,
requests.Timeout) as ex:
logger.error("Unable to connect to server (%s): %s", login_url, ex)
return False
disconnect()
¶
Removes the API token, disconnecting the user
Source code in lutris/api.py
def disconnect():
"""Removes the API token, disconnecting the user"""
for file_path in [API_KEY_FILE_PATH, USER_INFO_FILE_PATH]:
if system.path_exists(file_path):
os.remove(file_path)
get_api_games(game_slugs=None, page=1, service=None)
¶
Return all games from the Lutris API matching the given game slugs
Source code in lutris/api.py
def get_api_games(game_slugs=None, page=1, service=None):
"""Return all games from the Lutris API matching the given game slugs"""
if service:
response_data = get_game_service_api_page(service, game_slugs)
else:
response_data = get_game_api_page(game_slugs)
if not response_data:
return []
results = response_data.get("results", [])
while response_data.get("next"):
page_match = re.search(r"page=(\d+)", response_data["next"])
if page_match:
next_page = page_match.group(1)
else:
logger.error("No page found in %s", response_data["next"])
break
if service:
response_data = get_game_service_api_page(service, game_slugs, page=next_page)
else:
response_data = get_game_api_page(game_slugs, page=next_page)
if not response_data:
logger.warning("Unable to get response for page %s", next_page)
break
results += response_data.get("results")
return results
get_bundle(bundle)
¶
Retrieve a lutris bundle from the API
Source code in lutris/api.py
def get_bundle(bundle):
"""Retrieve a lutris bundle from the API"""
url = "/api/bundles/%s" % bundle
response = http.Request(settings.SITE_URL + url, headers={"Content-Type": "application/json"})
try:
response.get()
except http.HTTPError as ex:
logger.error("Unable to get bundle from API: %s", ex)
return None
response_data = response.json
return response_data.get("games", [])
get_game_api_page(game_slugs, page=1)
¶
Read a single page of games from the API and return the response
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_ids |
list |
list of game slugs |
required |
page |
str |
Page of results to get |
1 |
Source code in lutris/api.py
def get_game_api_page(game_slugs, page=1):
"""Read a single page of games from the API and return the response
Args:
game_ids (list): list of game slugs
page (str): Page of results to get
"""
url = settings.SITE_URL + "/api/games"
if int(page) > 1:
url += "?page={}".format(page)
if not game_slugs:
return []
payload = json.dumps({"games": game_slugs, "page": page}).encode("utf-8")
return get_http_response(url, payload)
get_game_installers(game_slug, revision=None)
¶
Get installers for a single game
Source code in lutris/api.py
def get_game_installers(game_slug, revision=None):
"""Get installers for a single game"""
if not game_slug:
raise ValueError("No game_slug provided. Can't query an installer")
if revision:
installer_url = settings.INSTALLER_REVISION_URL % (game_slug, revision)
else:
installer_url = settings.INSTALLER_URL % game_slug
logger.debug("Fetching installer %s", installer_url)
request = http.Request(installer_url)
request.get()
response = request.json
if response is None:
raise RuntimeError("Couldn't get installer at %s" % installer_url)
if not revision:
return response["results"]
# Revision requests return a single installer
return [response]
get_game_service_api_page(service, appids, page=1)
¶
Get matching Lutris games from a list of appids from a given service
Source code in lutris/api.py
def get_game_service_api_page(service, appids, page=1):
"""Get matching Lutris games from a list of appids from a given service"""
url = settings.SITE_URL + "/api/games/service/%s" % service
if int(page) > 1:
url += "?page={}".format(page)
if not appids:
return []
payload = json.dumps({"appids": appids}).encode("utf-8")
return get_http_response(url, payload)
get_http_response(url, payload)
¶
Source code in lutris/api.py
def get_http_response(url, payload):
response = http.Request(url, headers={"Content-Type": "application/json"})
try:
response.get(data=payload)
except http.HTTPError as ex:
logger.error("Unable to get games from API: %s", ex)
return None
if response.status_code != 200:
logger.error("API call failed: %s", response.status_code)
return None
return response.json
get_runners(runner_name)
¶
Return the available runners for a given runner name
Source code in lutris/api.py
def get_runners(runner_name):
"""Return the available runners for a given runner name"""
api_url = settings.SITE_URL + "/api/runners/" + runner_name
response = http.Request(api_url).get()
return response.json
get_user_info()
¶
Retrieves the user info to cache it locally
Source code in lutris/api.py
def get_user_info():
"""Retrieves the user info to cache it locally"""
credentials = read_api_key()
if not credentials:
return
url = settings.SITE_URL + "/api/users/me"
request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
response = request.get()
account_info = response.json
if not account_info:
logger.warning("Unable to fetch user info for %s", credentials["username"])
with open(USER_INFO_FILE_PATH, "w", encoding='utf-8') as token_file:
json.dump(account_info, token_file, indent=2)
parse_installer_url(url)
¶
Parses lutris: urls, extracting any info necessary to install or run a game.
Source code in lutris/api.py
def parse_installer_url(url):
"""
Parses `lutris:` urls, extracting any info necessary to install or run a game.
"""
action = None
try:
parsed_url = urllib.parse.urlparse(url, scheme="lutris")
except Exception: # pylint: disable=broad-except
logger.warning("Unable to parse url %s", url)
return False
if parsed_url.scheme != "lutris":
return False
url_path = parsed_url.path
if not url_path:
return False
# urlparse can't parse if the path only contain numbers
# workaround to remove the scheme manually:
if url_path.startswith("lutris:"):
url_path = url_path[7:]
url_parts = url_path.split("/")
if len(url_parts) == 2:
action = url_parts[0]
game_slug = url_parts[1]
elif len(url_parts) == 1:
game_slug = url_parts[0]
else:
raise ValueError("Invalid lutris url %s" % url)
# To link to service games, format a slug like <service>:<appid>
if ":" in game_slug:
service, appid = game_slug.split(":", maxsplit=1)
else:
service, appid = "", ""
revision = None
if parsed_url.query:
query = dict(urllib.parse.parse_qsl(parsed_url.query))
revision = query.get("revision")
return {
"game_slug": game_slug,
"revision": revision,
"action": action,
"service": service,
"appid": appid
}
read_api_key()
¶
Read the API token from disk
Source code in lutris/api.py
def read_api_key():
"""Read the API token from disk"""
if not system.path_exists(API_KEY_FILE_PATH):
return None
with open(API_KEY_FILE_PATH, "r", encoding='utf-8') as token_file:
api_string = token_file.read()
try:
username, token = api_string.split(":")
except ValueError:
logger.error("Unable to read Lutris token in %s", API_KEY_FILE_PATH)
return None
return {"token": token, "username": username}
search_games(query)
¶
Source code in lutris/api.py
def search_games(query):
if not query:
return {}
query = query.lower().strip()[:255]
url = "/api/games?%s" % urllib.parse.urlencode({"search": query, "with-installers": True})
response = http.Request(settings.SITE_URL + url, headers={"Content-Type": "application/json"})
try:
response.get()
except http.HTTPError as ex:
logger.error("Unable to get games from API: %s", ex)
return {}
return response.json
cache
¶
Module for handling the PGA cache
get_cache_path()
¶
Return the path of the PGA cache
Source code in lutris/cache.py
def get_cache_path():
"""Return the path of the PGA cache"""
pga_cache_path = settings.read_setting("pga_cache_path")
if pga_cache_path:
return os.path.expanduser(pga_cache_path)
return None
save_cache_path(path)
¶
Saves the PGA cache path to the settings
Source code in lutris/cache.py
def save_cache_path(path):
"""Saves the PGA cache path to the settings"""
settings.write_setting("pga_cache_path", path)
save_to_cache(source, destination)
¶
Copy a file or folder to the cache
Source code in lutris/cache.py
def save_to_cache(source, destination):
"""Copy a file or folder to the cache"""
if not source:
raise ValueError("Missing source")
if os.path.dirname(source) == destination:
logger.info("Skipping caching of %s, already cached in %s", source, destination)
return
if os.path.isdir(source):
# Copy folder recursively
merge_folders(source, destination)
else:
shutil.copy(source, destination)
logger.debug("Cached %s to %s", source, destination)
command
¶
Threading module, used to launch games while monitoring them.
WRAPPER_SCRIPT
¶
MonitoredCommand
¶
Exexcutes a commmand while keeping track of its state
Source code in lutris/command.py
class MonitoredCommand:
"""Exexcutes a commmand while keeping track of its state"""
fallback_cwd = "/tmp"
def __init__(
self,
command,
runner=None,
env=None,
term=None,
cwd=None,
include_processes=None,
exclude_processes=None,
log_buffer=None,
title=None,
): # pylint: disable=too-many-arguments
self.ready_state = True
self.env = self.get_environment(env)
self.accepted_return_code = "0"
self.command = command
self.runner = runner
self.stop_func = lambda: True
self.game_process = None
self.prevent_on_stop = False
self.return_code = None
self.terminal = term
self.is_running = True
self.error = None
self.log_handlers = [
self.log_handler_stdout,
self.log_handler_console_output,
]
self.set_log_buffer(log_buffer)
self.stdout_monitor = None
self.include_processes = include_processes or []
self.exclude_processes = exclude_processes or []
self.cwd = self.get_cwd(cwd)
self._stdout = io.StringIO()
self._title = title if title else command[0]
@property
def stdout(self):
return self._stdout.getvalue()
def get_wrapper_command(self):
"""Return launch arguments for the wrapper script"""
wrapper_command = [
WRAPPER_SCRIPT,
self._title,
str(len(self.include_processes)),
str(len(self.exclude_processes)),
] + self.include_processes + self.exclude_processes
if not self.terminal:
return wrapper_command + self.command
terminal_path = system.find_executable(self.terminal)
if not terminal_path:
raise RuntimeError("Couldn't find terminal %s" % self.terminal)
script_path = get_terminal_script(self.command, self.cwd, self.env)
return wrapper_command + [terminal_path, "-e", script_path]
def set_log_buffer(self, log_buffer):
"""Attach a TextBuffer to this command enables the buffer handler"""
if not log_buffer:
return
self.log_buffer = log_buffer
if self.log_handler_buffer not in self.log_handlers:
self.log_handlers.append(self.log_handler_buffer)
def get_cwd(self, cwd):
"""Return the current working dir of the game"""
if not cwd:
cwd = self.runner.working_dir if self.runner else None
return os.path.expanduser(cwd or "~")
@staticmethod
def get_environment(user_env):
"""Process the user provided environment variables for use as self.env"""
env = user_env or {}
# not clear why this needs to be added, the path is already added in
# the wrappper script.
env['PYTHONPATH'] = ':'.join(sys.path)
# Drop bad values of environment keys, those will confuse the Python
# interpreter.
env["LUTRIS_GAME_UUID"] = str(uuid.uuid4())
return {key: value for key, value in env.items() if "=" not in key}
def get_child_environment(self):
"""Returns the calculated environment for the child process."""
env = os.environ.copy()
env.update(self.env)
return env
def start(self):
"""Run the thread."""
for key, value in self.env.items():
logger.debug("%s=\"%s\"", key, value)
wrapper_command = self.get_wrapper_command()
env = self.get_child_environment()
self.game_process = self.execute_process(wrapper_command, env)
if not self.game_process:
logger.error("No game process available")
return
GLib.child_watch_add(self.game_process.pid, self.on_stop)
# make stdout nonblocking.
fileno = self.game_process.stdout.fileno()
fcntl.fcntl(fileno, fcntl.F_SETFL, fcntl.fcntl(fileno, fcntl.F_GETFL) | os.O_NONBLOCK)
self.stdout_monitor = GLib.io_add_watch(
self.game_process.stdout,
GLib.IO_IN | GLib.IO_HUP,
self.on_stdout_output,
)
def log_handler_stdout(self, line):
"""Add the line to this command's stdout attribute"""
self._stdout.write(line)
def log_handler_buffer(self, line):
"""Add the line to the associated LogBuffer object"""
self.log_buffer.insert(self.log_buffer.get_end_iter(), line, -1)
def log_handler_console_output(self, line): # pylint: disable=no-self-use
"""Print the line to stdout"""
with contextlib.suppress(BlockingIOError):
sys.stdout.write(line)
sys.stdout.flush()
def get_return_code(self):
"""Get the return code from the file written by the wrapper"""
return_code_path = "/tmp/lutris-%s" % self.env["LUTRIS_GAME_UUID"]
if os.path.exists(return_code_path):
with open(return_code_path, encoding='utf-8') as return_code_file:
return_code = return_code_file.read()
os.unlink(return_code_path)
else:
return_code = ''
logger.warning("No file %s", return_code_path)
return return_code
def on_stop(self, pid, _user_data):
"""Callback registered on game process termination"""
if self.prevent_on_stop: # stop() already in progress
return False
self.game_process.wait()
self.return_code = self.get_return_code()
self.is_running = False
logger.debug("Process %s has terminated with code %s", pid, self.return_code)
resume_stop = self.stop()
if not resume_stop:
logger.info("Full shutdown prevented")
return False
return False
def on_stdout_output(self, stdout, condition):
"""Called by the stdout monitor to dispatch output to log handlers"""
if condition == GLib.IO_HUP:
self.stdout_monitor = None
return False
if not self.is_running:
return False
try:
line = stdout.read(262144).decode("utf-8", errors="ignore")
except ValueError:
# file_desc might be closed
return True
if "winemenubuilder.exe" in line:
return True
for log_handler in self.log_handlers:
log_handler(line)
return True
def execute_process(self, command, env=None):
"""Execute and return a subprocess"""
if self.cwd and not system.path_exists(self.cwd):
try:
os.makedirs(self.cwd)
except OSError:
logger.error("Failed to create working directory, falling back to %s", self.fallback_cwd)
self.cwd = "/tmp"
try:
return subprocess.Popen( # pylint: disable=consider-using-with
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=self.cwd,
env=env,
)
except OSError as ex:
logger.exception("Failed to execute %s: %s", " ".join(command), ex)
self.error = ex.strerror
def stop(self):
"""Stops the current game process and cleans up the instance"""
# Prevent stop() being called again by the process exiting
self.prevent_on_stop = True
try:
self.game_process.terminate()
except ProcessLookupError:
# process already dead.
pass
resume_stop = self.stop_func()
if not resume_stop:
logger.warning("Stop execution halted by demand of stop_func")
return False
if self.stdout_monitor:
GLib.source_remove(self.stdout_monitor)
self.stdout_monitor = None
self.is_running = False
self.ready_state = False
return True
fallback_cwd
¶
stdout
property
readonly
¶
__init__(self, command, runner=None, env=None, term=None, cwd=None, include_processes=None, exclude_processes=None, log_buffer=None, title=None)
special
¶
Source code in lutris/command.py
def __init__(
self,
command,
runner=None,
env=None,
term=None,
cwd=None,
include_processes=None,
exclude_processes=None,
log_buffer=None,
title=None,
): # pylint: disable=too-many-arguments
self.ready_state = True
self.env = self.get_environment(env)
self.accepted_return_code = "0"
self.command = command
self.runner = runner
self.stop_func = lambda: True
self.game_process = None
self.prevent_on_stop = False
self.return_code = None
self.terminal = term
self.is_running = True
self.error = None
self.log_handlers = [
self.log_handler_stdout,
self.log_handler_console_output,
]
self.set_log_buffer(log_buffer)
self.stdout_monitor = None
self.include_processes = include_processes or []
self.exclude_processes = exclude_processes or []
self.cwd = self.get_cwd(cwd)
self._stdout = io.StringIO()
self._title = title if title else command[0]
execute_process(self, command, env=None)
¶
Execute and return a subprocess
Source code in lutris/command.py
def execute_process(self, command, env=None):
"""Execute and return a subprocess"""
if self.cwd and not system.path_exists(self.cwd):
try:
os.makedirs(self.cwd)
except OSError:
logger.error("Failed to create working directory, falling back to %s", self.fallback_cwd)
self.cwd = "/tmp"
try:
return subprocess.Popen( # pylint: disable=consider-using-with
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=self.cwd,
env=env,
)
except OSError as ex:
logger.exception("Failed to execute %s: %s", " ".join(command), ex)
self.error = ex.strerror
get_child_environment(self)
¶
Returns the calculated environment for the child process.
Source code in lutris/command.py
def get_child_environment(self):
"""Returns the calculated environment for the child process."""
env = os.environ.copy()
env.update(self.env)
return env
get_cwd(self, cwd)
¶
Return the current working dir of the game
Source code in lutris/command.py
def get_cwd(self, cwd):
"""Return the current working dir of the game"""
if not cwd:
cwd = self.runner.working_dir if self.runner else None
return os.path.expanduser(cwd or "~")
get_environment(user_env)
staticmethod
¶
Process the user provided environment variables for use as self.env
Source code in lutris/command.py
@staticmethod
def get_environment(user_env):
"""Process the user provided environment variables for use as self.env"""
env = user_env or {}
# not clear why this needs to be added, the path is already added in
# the wrappper script.
env['PYTHONPATH'] = ':'.join(sys.path)
# Drop bad values of environment keys, those will confuse the Python
# interpreter.
env["LUTRIS_GAME_UUID"] = str(uuid.uuid4())
return {key: value for key, value in env.items() if "=" not in key}
get_return_code(self)
¶
Get the return code from the file written by the wrapper
Source code in lutris/command.py
def get_return_code(self):
"""Get the return code from the file written by the wrapper"""
return_code_path = "/tmp/lutris-%s" % self.env["LUTRIS_GAME_UUID"]
if os.path.exists(return_code_path):
with open(return_code_path, encoding='utf-8') as return_code_file:
return_code = return_code_file.read()
os.unlink(return_code_path)
else:
return_code = ''
logger.warning("No file %s", return_code_path)
return return_code
get_wrapper_command(self)
¶
Return launch arguments for the wrapper script
Source code in lutris/command.py
def get_wrapper_command(self):
"""Return launch arguments for the wrapper script"""
wrapper_command = [
WRAPPER_SCRIPT,
self._title,
str(len(self.include_processes)),
str(len(self.exclude_processes)),
] + self.include_processes + self.exclude_processes
if not self.terminal:
return wrapper_command + self.command
terminal_path = system.find_executable(self.terminal)
if not terminal_path:
raise RuntimeError("Couldn't find terminal %s" % self.terminal)
script_path = get_terminal_script(self.command, self.cwd, self.env)
return wrapper_command + [terminal_path, "-e", script_path]
log_handler_buffer(self, line)
¶
Add the line to the associated LogBuffer object
Source code in lutris/command.py
def log_handler_buffer(self, line):
"""Add the line to the associated LogBuffer object"""
self.log_buffer.insert(self.log_buffer.get_end_iter(), line, -1)
log_handler_console_output(self, line)
¶
Print the line to stdout
Source code in lutris/command.py
def log_handler_console_output(self, line): # pylint: disable=no-self-use
"""Print the line to stdout"""
with contextlib.suppress(BlockingIOError):
sys.stdout.write(line)
sys.stdout.flush()
log_handler_stdout(self, line)
¶
Add the line to this command's stdout attribute
Source code in lutris/command.py
def log_handler_stdout(self, line):
"""Add the line to this command's stdout attribute"""
self._stdout.write(line)
on_stdout_output(self, stdout, condition)
¶
Called by the stdout monitor to dispatch output to log handlers
Source code in lutris/command.py
def on_stdout_output(self, stdout, condition):
"""Called by the stdout monitor to dispatch output to log handlers"""
if condition == GLib.IO_HUP:
self.stdout_monitor = None
return False
if not self.is_running:
return False
try:
line = stdout.read(262144).decode("utf-8", errors="ignore")
except ValueError:
# file_desc might be closed
return True
if "winemenubuilder.exe" in line:
return True
for log_handler in self.log_handlers:
log_handler(line)
return True
on_stop(self, pid, _user_data)
¶
Callback registered on game process termination
Source code in lutris/command.py
def on_stop(self, pid, _user_data):
"""Callback registered on game process termination"""
if self.prevent_on_stop: # stop() already in progress
return False
self.game_process.wait()
self.return_code = self.get_return_code()
self.is_running = False
logger.debug("Process %s has terminated with code %s", pid, self.return_code)
resume_stop = self.stop()
if not resume_stop:
logger.info("Full shutdown prevented")
return False
return False
set_log_buffer(self, log_buffer)
¶
Attach a TextBuffer to this command enables the buffer handler
Source code in lutris/command.py
def set_log_buffer(self, log_buffer):
"""Attach a TextBuffer to this command enables the buffer handler"""
if not log_buffer:
return
self.log_buffer = log_buffer
if self.log_handler_buffer not in self.log_handlers:
self.log_handlers.append(self.log_handler_buffer)
start(self)
¶
Run the thread.
Source code in lutris/command.py
def start(self):
"""Run the thread."""
for key, value in self.env.items():
logger.debug("%s=\"%s\"", key, value)
wrapper_command = self.get_wrapper_command()
env = self.get_child_environment()
self.game_process = self.execute_process(wrapper_command, env)
if not self.game_process:
logger.error("No game process available")
return
GLib.child_watch_add(self.game_process.pid, self.on_stop)
# make stdout nonblocking.
fileno = self.game_process.stdout.fileno()
fcntl.fcntl(fileno, fcntl.F_SETFL, fcntl.fcntl(fileno, fcntl.F_GETFL) | os.O_NONBLOCK)
self.stdout_monitor = GLib.io_add_watch(
self.game_process.stdout,
GLib.IO_IN | GLib.IO_HUP,
self.on_stdout_output,
)
stop(self)
¶
Stops the current game process and cleans up the instance
Source code in lutris/command.py
def stop(self):
"""Stops the current game process and cleans up the instance"""
# Prevent stop() being called again by the process exiting
self.prevent_on_stop = True
try:
self.game_process.terminate()
except ProcessLookupError:
# process already dead.
pass
resume_stop = self.stop_func()
if not resume_stop:
logger.warning("Stop execution halted by demand of stop_func")
return False
if self.stdout_monitor:
GLib.source_remove(self.stdout_monitor)
self.stdout_monitor = None
self.is_running = False
self.ready_state = False
return True
exec_command(command)
¶
Execute arbitrary command in a MonitoredCommand
Used by the --exec command line flag.
Source code in lutris/command.py
def exec_command(command):
"""Execute arbitrary command in a MonitoredCommand
Used by the --exec command line flag.
"""
command = MonitoredCommand(shlex.split(command), env=runtime.get_env())
command.start()
return command
get_wrapper_script_location()
¶
Return absolute path of lutris-wrapper script
Source code in lutris/command.py
def get_wrapper_script_location():
"""Return absolute path of lutris-wrapper script"""
wrapper_relpath = "share/lutris/bin/lutris-wrapper"
candidates = [
os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "..")),
os.path.dirname(os.path.dirname(settings.__file__)),
"/usr",
"/usr/local",
]
for candidate in candidates:
wrapper_abspath = os.path.join(candidate, wrapper_relpath)
if os.path.isfile(wrapper_abspath):
return wrapper_abspath
raise FileNotFoundError("Couldn't find lutris-wrapper script in any of the expected locations")
config
¶
Handle the game, runner and global system configurations.
LutrisConfig
¶
Class where all the configuration handling happens.
Description¶
Lutris' configuration uses a cascading mechanism where each higher, more specific level overrides the lower ones
The levels are (highest to lowest): game, runner and system.
Each level has its own set of options (config section), available to and
overridden by upper levels:
level | Config sections
-------|----------------------
game | system, runner, game
runner | system, runner
system | system
The config levels are stored in separate YAML format text files.
Usage¶
The config level will be auto set depending on what you pass to init: - For game level, pass game_config_id and optionally runner_slug (better perfs) - For runner level, pass runner_slug - For system level, pass nothing If need be, you can pass the level manually.
To read, use the config sections dicts: game_config, runner_config and system_config.
To write, modify the relevant raw_*_config section dict, then run
save().
Source code in lutris/config.py
class LutrisConfig:
"""Class where all the configuration handling happens.
Description
===========
Lutris' configuration uses a cascading mechanism where
each higher, more specific level overrides the lower ones
The levels are (highest to lowest): `game`, `runner` and `system`.
Each level has its own set of options (config section), available to and
overridden by upper levels:
```
level | Config sections
-------|----------------------
game | system, runner, game
runner | system, runner
system | system
```
Example: if requesting runner options at game level, their returned value
will be from the game level config if it's set at this level; if not it
will be the value from runner level if available; and if not, the default
value set in the runner's module, or None.
The config levels are stored in separate YAML format text files.
Usage
=====
The config level will be auto set depending on what you pass to __init__:
- For game level, pass game_config_id and optionally runner_slug (better perfs)
- For runner level, pass runner_slug
- For system level, pass nothing
If need be, you can pass the level manually.
To read, use the config sections dicts: game_config, runner_config and
system_config.
To write, modify the relevant `raw_*_config` section dict, then run
`save()`.
"""
def __init__(self, runner_slug=None, game_config_id=None, level=None):
self.game_config_id = game_config_id
if runner_slug:
self.runner_slug = str(runner_slug)
else:
self.runner_slug = runner_slug
# Cascaded config sections (for reading)
self.game_config = {}
self.runner_config = {}
self.system_config = {}
# Raw (non-cascaded) sections (for writing)
self.raw_game_config = {}
self.raw_runner_config = {}
self.raw_system_config = {}
self.raw_config = {}
# Set config level
self.level = level
if not level:
if game_config_id:
self.level = "game"
elif runner_slug:
self.level = "runner"
else:
self.level = "system"
self.initialize_config()
def __repr__(self):
return "LutrisConfig(level=%s, game_config_id=%s, runner=%s)" % (
self.level,
self.game_config_id,
self.runner_slug,
)
@property
def system_config_path(self):
return os.path.join(settings.CONFIG_DIR, "system.yml")
@property
def runner_config_path(self):
if not self.runner_slug:
return None
return os.path.join(settings.CONFIG_DIR, "runners/%s.yml" % self.runner_slug)
@property
def game_config_path(self):
if not self.game_config_id:
return None
return os.path.join(settings.CONFIG_DIR, "games/%s.yml" % self.game_config_id)
def initialize_config(self):
"""Init and load config files"""
self.game_level = {"system": {}, self.runner_slug: {}, "game": {}}
self.runner_level = {"system": {}, self.runner_slug: {}}
self.system_level = {"system": {}}
self.game_level.update(read_yaml_from_file(self.game_config_path))
self.runner_level.update(read_yaml_from_file(self.runner_config_path))
self.system_level.update(read_yaml_from_file(self.system_config_path))
self.update_cascaded_config()
self.update_raw_config()
def update_cascaded_config(self):
if self.system_level.get("system") is None:
self.system_level["system"] = {}
self.system_config.clear()
self.system_config.update(self.get_defaults("system"))
self.system_config.update(self.system_level.get("system"))
if self.level in ["runner", "game"] and self.runner_slug:
if self.runner_level.get(self.runner_slug) is None:
self.runner_level[self.runner_slug] = {}
if self.runner_level.get("system") is None:
self.runner_level["system"] = {}
self.runner_config.clear()
self.runner_config.update(self.get_defaults("runner"))
self.runner_config.update(self.runner_level.get(self.runner_slug))
self.merge_to_system_config(self.runner_level.get("system"))
if self.level == "game" and self.runner_slug:
if self.game_level.get("game") is None:
self.game_level["game"] = {}
if self.game_level.get(self.runner_slug) is None:
self.game_level[self.runner_slug] = {}
if self.game_level.get("system") is None:
self.game_level["system"] = {}
self.game_config.clear()
self.game_config.update(self.get_defaults("game"))
self.game_config.update(self.game_level.get("game"))
self.runner_config.update(self.game_level.get(self.runner_slug))
self.merge_to_system_config(self.game_level.get("system"))
def merge_to_system_config(self, config):
"""Merge a configuration to the system configuation"""
if not config:
return
existing_env = None
if self.system_config.get("env") and "env" in config:
existing_env = self.system_config["env"]
self.system_config.update(config)
if existing_env:
self.system_config["env"] = existing_env
self.system_config["env"].update(config["env"])
def update_raw_config(self):
# Select the right level of config
if self.level == "game":
raw_config = self.game_level
elif self.level == "runner":
raw_config = self.runner_level
else:
raw_config = self.system_level
# Load config sections
self.raw_system_config = raw_config["system"]
if self.level in ["runner", "game"]:
self.raw_runner_config = raw_config[self.runner_slug]
if self.level == "game":
self.raw_game_config = raw_config["game"]
self.raw_config = raw_config
def remove(self):
"""Delete the configuration file from disk."""
if not self.game_config_path:
raise RuntimeError("Tried to remove a non-existent config")
if not path_exists(self.game_config_path):
logger.debug("No config file at %s", self.game_config_path)
return
os.remove(self.game_config_path)
logger.debug("Removed config %s", self.game_config_path)
def save(self):
"""Save configuration file according to its type"""
if self.level == "system":
config = self.system_level
config_path = self.system_config_path
elif self.level == "runner":
config = self.runner_level
config_path = self.runner_config_path
elif self.level == "game":
config = self.game_level
config_path = self.game_config_path
else:
raise ValueError("Invalid config level '%s'" % self.level)
logger.debug("Saving %s config to %s", self, config_path)
write_yaml_to_file(config, config_path)
self.initialize_config()
def get_defaults(self, options_type):
"""Return a dict of options' default value."""
options_dict = self.options_as_dict(options_type)
defaults = {}
for option, params in options_dict.items():
if "default" in params:
defaults[option] = params["default"]
return defaults
def options_as_dict(self, options_type):
"""Convert the option list to a dict with option name as keys"""
if options_type == "system":
options = (
sysoptions.with_runner_overrides(self.runner_slug) if self.runner_slug else sysoptions.system_options
)
else:
if not self.runner_slug:
return None
attribute_name = options_type + "_options"
try:
runner = import_runner(self.runner_slug)
except InvalidRunner:
options = {}
else:
if not getattr(runner, attribute_name):
runner = runner()
options = getattr(runner, attribute_name)
return dict((opt["option"], opt) for opt in options)
game_config_path
property
readonly
¶
runner_config_path
property
readonly
¶
system_config_path
property
readonly
¶
__init__(self, runner_slug=None, game_config_id=None, level=None)
special
¶
Source code in lutris/config.py
def __init__(self, runner_slug=None, game_config_id=None, level=None):
self.game_config_id = game_config_id
if runner_slug:
self.runner_slug = str(runner_slug)
else:
self.runner_slug = runner_slug
# Cascaded config sections (for reading)
self.game_config = {}
self.runner_config = {}
self.system_config = {}
# Raw (non-cascaded) sections (for writing)
self.raw_game_config = {}
self.raw_runner_config = {}
self.raw_system_config = {}
self.raw_config = {}
# Set config level
self.level = level
if not level:
if game_config_id:
self.level = "game"
elif runner_slug:
self.level = "runner"
else:
self.level = "system"
self.initialize_config()
__repr__(self)
special
¶
Source code in lutris/config.py
def __repr__(self):
return "LutrisConfig(level=%s, game_config_id=%s, runner=%s)" % (
self.level,
self.game_config_id,
self.runner_slug,
)
get_defaults(self, options_type)
¶
Return a dict of options' default value.
Source code in lutris/config.py
def get_defaults(self, options_type):
"""Return a dict of options' default value."""
options_dict = self.options_as_dict(options_type)
defaults = {}
for option, params in options_dict.items():
if "default" in params:
defaults[option] = params["default"]
return defaults
initialize_config(self)
¶
Init and load config files
Source code in lutris/config.py
def initialize_config(self):
"""Init and load config files"""
self.game_level = {"system": {}, self.runner_slug: {}, "game": {}}
self.runner_level = {"system": {}, self.runner_slug: {}}
self.system_level = {"system": {}}
self.game_level.update(read_yaml_from_file(self.game_config_path))
self.runner_level.update(read_yaml_from_file(self.runner_config_path))
self.system_level.update(read_yaml_from_file(self.system_config_path))
self.update_cascaded_config()
self.update_raw_config()
merge_to_system_config(self, config)
¶
Merge a configuration to the system configuation
Source code in lutris/config.py
def merge_to_system_config(self, config):
"""Merge a configuration to the system configuation"""
if not config:
return
existing_env = None
if self.system_config.get("env") and "env" in config:
existing_env = self.system_config["env"]
self.system_config.update(config)
if existing_env:
self.system_config["env"] = existing_env
self.system_config["env"].update(config["env"])
options_as_dict(self, options_type)
¶
Convert the option list to a dict with option name as keys
Source code in lutris/config.py
def options_as_dict(self, options_type):
"""Convert the option list to a dict with option name as keys"""
if options_type == "system":
options = (
sysoptions.with_runner_overrides(self.runner_slug) if self.runner_slug else sysoptions.system_options
)
else:
if not self.runner_slug:
return None
attribute_name = options_type + "_options"
try:
runner = import_runner(self.runner_slug)
except InvalidRunner:
options = {}
else:
if not getattr(runner, attribute_name):
runner = runner()
options = getattr(runner, attribute_name)
return dict((opt["option"], opt) for opt in options)
remove(self)
¶
Delete the configuration file from disk.
Source code in lutris/config.py
def remove(self):
"""Delete the configuration file from disk."""
if not self.game_config_path:
raise RuntimeError("Tried to remove a non-existent config")
if not path_exists(self.game_config_path):
logger.debug("No config file at %s", self.game_config_path)
return
os.remove(self.game_config_path)
logger.debug("Removed config %s", self.game_config_path)
save(self)
¶
Save configuration file according to its type
Source code in lutris/config.py
def save(self):
"""Save configuration file according to its type"""
if self.level == "system":
config = self.system_level
config_path = self.system_config_path
elif self.level == "runner":
config = self.runner_level
config_path = self.runner_config_path
elif self.level == "game":
config = self.game_level
config_path = self.game_config_path
else:
raise ValueError("Invalid config level '%s'" % self.level)
logger.debug("Saving %s config to %s", self, config_path)
write_yaml_to_file(config, config_path)
self.initialize_config()
update_cascaded_config(self)
¶
Source code in lutris/config.py
def update_cascaded_config(self):
if self.system_level.get("system") is None:
self.system_level["system"] = {}
self.system_config.clear()
self.system_config.update(self.get_defaults("system"))
self.system_config.update(self.system_level.get("system"))
if self.level in ["runner", "game"] and self.runner_slug:
if self.runner_level.get(self.runner_slug) is None:
self.runner_level[self.runner_slug] = {}
if self.runner_level.get("system") is None:
self.runner_level["system"] = {}
self.runner_config.clear()
self.runner_config.update(self.get_defaults("runner"))
self.runner_config.update(self.runner_level.get(self.runner_slug))
self.merge_to_system_config(self.runner_level.get("system"))
if self.level == "game" and self.runner_slug:
if self.game_level.get("game") is None:
self.game_level["game"] = {}
if self.game_level.get(self.runner_slug) is None:
self.game_level[self.runner_slug] = {}
if self.game_level.get("system") is None:
self.game_level["system"] = {}
self.game_config.clear()
self.game_config.update(self.get_defaults("game"))
self.game_config.update(self.game_level.get("game"))
self.runner_config.update(self.game_level.get(self.runner_slug))
self.merge_to_system_config(self.game_level.get("system"))
update_raw_config(self)
¶
Source code in lutris/config.py
def update_raw_config(self):
# Select the right level of config
if self.level == "game":
raw_config = self.game_level
elif self.level == "runner":
raw_config = self.runner_level
else:
raw_config = self.system_level
# Load config sections
self.raw_system_config = raw_config["system"]
if self.level in ["runner", "game"]:
self.raw_runner_config = raw_config[self.runner_slug]
if self.level == "game":
self.raw_game_config = raw_config["game"]
self.raw_config = raw_config
duplicate_game_config(game_slug, source_config_id)
¶
Copies an existing configuration file, giving it a new id that this function returns.
Source code in lutris/config.py
def duplicate_game_config(game_slug, source_config_id):
"""Copies an existing configuration file, giving it a new id that this
function returns."""
new_config_id = make_game_config_id(game_slug)
src_path = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % source_config_id)
dest_path = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % new_config_id)
copyfile(src_path, dest_path)
return new_config_id
make_game_config_id(game_slug)
¶
Return an unique config id to avoid clashes between multiple games
Source code in lutris/config.py
def make_game_config_id(game_slug):
"""Return an unique config id to avoid clashes between multiple games"""
return "{}-{}".format(game_slug, int(time.time()))
write_game_config(game_slug, config)
¶
Writes a game config to disk
Source code in lutris/config.py
def write_game_config(game_slug, config):
"""Writes a game config to disk"""
configpath = make_game_config_id(game_slug)
logger.debug("Writing game config to %s", configpath)
config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath)
write_yaml_to_file(config, config_filename)
return configpath
database
special
¶
categories
¶
add_category(category_name)
¶
Add a category to the database
Source code in lutris/database/categories.py
def add_category(category_name):
"""Add a category to the database"""
return sql.db_insert(settings.PGA_DB, "categories", {"name": category_name})
add_game_to_category(game_id, category_id)
¶
Add a category to a game
Source code in lutris/database/categories.py
def add_game_to_category(game_id, category_id):
"""Add a category to a game"""
return sql.db_insert(settings.PGA_DB, "games_categories", {"game_id": game_id, "category_id": category_id})
get_categories()
¶
Get the list of every category in database.
Source code in lutris/database/categories.py
def get_categories():
"""Get the list of every category in database."""
return sql.db_select(settings.PGA_DB, "categories",)
get_categories_in_game(game_id)
¶
Get the categories of a game in database.
Source code in lutris/database/categories.py
def get_categories_in_game(game_id):
"""Get the categories of a game in database."""
query = (
"select categories.name from categories "
"JOIN games_categories ON categories.id = games_categories.category_id "
"JOIN games ON games.id = games_categories.game_id "
"WHERE games.id=?"
)
return [
category["name"]
for category in sql.db_query(settings.PGA_DB, query, (game_id,))
]
get_category(name)
¶
Return a category by name
Source code in lutris/database/categories.py
def get_category(name):
"""Return a category by name"""
categories = sql.db_select(settings.PGA_DB, "categories", condition=("name", name))
if categories:
return categories[0]
get_game_ids_for_category(category_name)
¶
Get the ids of games in database.
Source code in lutris/database/categories.py
def get_game_ids_for_category(category_name):
"""Get the ids of games in database."""
query = (
"select game_id from games_categories "
"JOIN categories ON categories.id = games_categories.category_id "
"WHERE categories.name=?"
)
return [
game["game_id"]
for game in sql.db_query(settings.PGA_DB, query, (category_name, ))
]
remove_category_from_game(game_id, category_id)
¶
Remove a category from a game
Source code in lutris/database/categories.py
def remove_category_from_game(game_id, category_id):
"""Remove a category from a game"""
query = "DELETE FROM games_categories WHERE category_id=? AND game_id=?"
with sql.db_cursor(settings.PGA_DB) as cursor:
sql.cursor_execute(cursor, query, (category_id, game_id))
games
¶
add_game(**game_data)
¶
Add a game to the PGA database.
Source code in lutris/database/games.py
def add_game(**game_data):
"""Add a game to the PGA database."""
game_data["installed_at"] = int(time.time())
if "slug" not in game_data:
game_data["slug"] = slugify(game_data["name"])
return sql.db_insert(settings.PGA_DB, "games", game_data)
add_games_bulk(games)
¶
Add a list of games to the PGA database. The dicts must have an identical set of keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
games |
list |
list of games in dict format |
required |
Returns:
| Type | Description |
|---|---|
list |
List of inserted game ids |
Source code in lutris/database/games.py
def add_games_bulk(games):
"""
Add a list of games to the PGA database.
The dicts must have an identical set of keys.
Args:
games (list): list of games in dict format
Returns:
list: List of inserted game ids
"""
return [sql.db_insert(settings.PGA_DB, "games", game) for game in games]
add_or_update(**params)
¶
Add a game to the PGA or update an existing one
If an 'id' is provided in the parameters then it will try to match it, otherwise it will try matching by slug, creating one when possible.
Source code in lutris/database/games.py
def add_or_update(**params):
"""Add a game to the PGA or update an existing one
If an 'id' is provided in the parameters then it
will try to match it, otherwise it will try matching
by slug, creating one when possible.
"""
game_id = get_matching_game(params)
if game_id:
params["id"] = game_id
sql.db_update(settings.PGA_DB, "games", params, {"id": game_id})
return game_id
return add_game(**params)
delete_game(game_id)
¶
Delete a game from the PGA.
Source code in lutris/database/games.py
def delete_game(game_id):
"""Delete a game from the PGA."""
sql.db_delete(settings.PGA_DB, "games", "id", game_id)
get_game_by_field(value, field='slug')
¶
Query a game based on a database field
Source code in lutris/database/games.py
def get_game_by_field(value, field="slug"):
"""Query a game based on a database field"""
if field not in ("slug", "installer_slug", "id", "configpath", "name"):
raise ValueError("Can't query by field '%s'" % field)
game_result = sql.db_select(settings.PGA_DB, "games", condition=(field, value))
if game_result:
return game_result[0]
return {}
get_game_for_service(service, appid)
¶
Source code in lutris/database/games.py
def get_game_for_service(service, appid):
existing_games = get_games(filters={"service_id": appid, "service": service})
if existing_games:
return existing_games[0]
get_games(searches=None, filters=None, excludes=None, sorts=None)
¶
Source code in lutris/database/games.py
def get_games(
searches=None,
filters=None,
excludes=None,
sorts=None
):
return sql.filtered_query(
settings.PGA_DB,
"games",
searches=searches,
filters=filters,
excludes=excludes,
sorts=sorts
)
get_games_by_ids(game_ids)
¶
Source code in lutris/database/games.py
def get_games_by_ids(game_ids):
# sqlite limits the number of query parameters to 999, to
# bypass that limitation, divide the query in chunks
size = 999
return list(
chain.from_iterable(
[
get_games_where(id__in=list(game_ids)[page * size:page * size + size])
for page in range(math.ceil(len(game_ids) / size))
]
)
)
get_games_by_runner(runner)
¶
Return all games using a specific runner
Source code in lutris/database/games.py
def get_games_by_runner(runner):
"""Return all games using a specific runner"""
return sql.db_select(settings.PGA_DB, "games", condition=("runner", runner))
get_games_by_slug(slug)
¶
Return all games using a specific slug
Source code in lutris/database/games.py
def get_games_by_slug(slug):
"""Return all games using a specific slug"""
return sql.db_select(settings.PGA_DB, "games", condition=("slug", slug))
get_games_where(**conditions)
¶
Query games table based on conditions
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conditions |
dict |
named arguments with each field matches its desired value. |
{} |
Special |
values for field names can be used |
|
required |
Returns:
| Type | Description |
|---|---|
list |
Rows matching the query |
Source code in lutris/database/games.py
def get_games_where(**conditions):
"""
Query games table based on conditions
Args:
conditions (dict): named arguments with each field matches its desired value.
Special values for field names can be used:
<field>__isnull will return rows where `field` is NULL if the value is True
<field>__not will invert the condition using `!=` instead of `=`
<field>__in will match rows for every value of `value`, which should be an iterable
Returns:
list: Rows matching the query
"""
query = "select * from games"
condition_fields = []
condition_values = []
for field, value in conditions.items():
field, *extra_conditions = field.split("__")
if extra_conditions:
extra_condition = extra_conditions[0]
if extra_condition == "isnull":
condition_fields.append("{} is {} null".format(field, "" if value else "not"))
if extra_condition == "not":
condition_fields.append("{} != ?".format(field))
condition_values.append(value)
if extra_condition == "in":
if not hasattr(value, "__iter__"):
raise ValueError("Value should be an iterable (%s given)" % value)
if len(value) > 999:
raise ValueError("SQLite limnited to a maximum of 999 parameters.")
if value:
condition_fields.append("{} in ({})".format(field, ", ".join("?" * len(value)) or ""))
condition_values = list(chain(condition_values, value))
else:
condition_fields.append("{} = ?".format(field))
condition_values.append(value)
condition = " AND ".join(condition_fields)
if condition:
query = " WHERE ".join((query, condition))
else:
# Inspect and document why we should return
# an empty list when no condition is present.
return []
return sql.db_query(settings.PGA_DB, query, tuple(condition_values))
get_matching_game(params)
¶
Tries to match given parameters with an existing game
Source code in lutris/database/games.py
def get_matching_game(params):
"""Tries to match given parameters with an existing game"""
# Always match by ID if provided
if params.get("id"):
game = get_game_by_field(params["id"], "id")
if game:
return game["id"]
logger.warning("Game ID %s provided but couldn't be matched", params["id"])
slug = params.get("slug") or slugify(params.get("name"))
if not slug:
raise ValueError("Can't add or update without an identifier")
for game in get_games_by_slug(slug):
if game["installed"]:
if game["configpath"] == params.get("configpath"):
return game["id"]
else:
if (game["runner"] == params.get("runner") or not all([params.get("runner"), game["runner"]])):
return game["id"]
return None
get_service_games(service)
¶
Return the list of all installed games for a service
Source code in lutris/database/games.py
def get_service_games(service):
"""Return the list of all installed games for a service"""
global _SERVICE_CACHE_ACCESSED
previous_cache_accessed = _SERVICE_CACHE_ACCESSED or 0
_SERVICE_CACHE_ACCESSED = time.time()
if service not in _SERVICE_CACHE or _SERVICE_CACHE_ACCESSED - previous_cache_accessed > 1:
if service == "lutris":
_SERVICE_CACHE[service] = [game["slug"] for game in get_games(filters={"installed": "1"})]
else:
_SERVICE_CACHE[service] = [
game["service_id"] for game in get_games(filters={"service": service, "installed": "1"})
]
return _SERVICE_CACHE[service]
get_unusued_game_name(game_name)
¶
Returns the given name, but if this name is already used by an installed game, this adds a number to it to make it unique.
Source code in lutris/database/games.py
def get_unusued_game_name(game_name):
"""Returns the given name, but if this name is already used by an installed
game, this adds a number to it to make it unique."""
def is_name_in_use(name):
"""Queries the database to see if a given is in use by an installed
game."""
existing_game = get_game_by_field(assigned_name, "name")
return existing_game and existing_game["installed"]
assigned_name = game_name
assigned_index = 1
while is_name_in_use(assigned_name):
assigned_index += 1
assigned_name = f"{game_name} {assigned_index}"
return assigned_name
get_used_platforms()
¶
Return a list of platforms currently in use
Source code in lutris/database/games.py
def get_used_platforms():
"""Return a list of platforms currently in use"""
with sql.db_cursor(settings.PGA_DB) as cursor:
query = (
"select distinct platform from games "
"where platform is not null and platform is not '' order by platform"
)
rows = cursor.execute(query)
results = rows.fetchall()
return [result[0] for result in results if result[0]]
get_used_runners()
¶
Return a list of the runners in use by installed games.
Source code in lutris/database/games.py
def get_used_runners():
"""Return a list of the runners in use by installed games."""
with sql.db_cursor(settings.PGA_DB) as cursor:
query = "select distinct runner from games where runner is not null order by runner"
rows = cursor.execute(query)
results = rows.fetchall()
return [result[0] for result in results if result[0]]
schema
¶
DATABASE
¶
create_table(name, schema)
¶
Creates a new table in the database
Source code in lutris/database/schema.py
def create_table(name, schema):
"""Creates a new table in the database"""
fields = ", ".join([field_to_string(**f) for f in schema])
query = "CREATE TABLE IF NOT EXISTS %s (%s)" % (name, fields)
logger.debug("[PGAQuery] %s", query)
with sql.db_cursor(settings.PGA_DB) as cursor:
cursor.execute(query)
field_to_string(name='', type='', indexed=False, unique=False)
¶
Converts a python based table definition to it's SQL statement
Source code in lutris/database/schema.py
def field_to_string(name="", type="", indexed=False, unique=False): # pylint: disable=redefined-builtin
"""Converts a python based table definition to it's SQL statement"""
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
if unique:
field_query += " UNIQUE"
return field_query
get_schema(tablename)
¶
Fields
- position
- name
- type
- not null
- default
- indexed
Source code in lutris/database/schema.py
def get_schema(tablename):
"""
Fields:
- position
- name
- type
- not null
- default
- indexed
"""
tables = []
query = "pragma table_info('%s')" % tablename
with sql.db_cursor(settings.PGA_DB) as cursor:
for row in cursor.execute(query).fetchall():
field = {
"name": row[1],
"type": row[2],
"not_null": row[3],
"default": row[4],
"indexed": row[5],
}
tables.append(field)
return tables
migrate(table, schema)
¶
Compare a database table with the reference model and make necessary changes
This is very basic and only the needed features have been implemented (adding columns)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table |
str |
Name of the table to migrate |
required |
schema |
dict |
Reference schema for the table |
required |
Returns:
| Type | Description |
|---|---|
list |
The list of column names that have been added |
Source code in lutris/database/schema.py
def migrate(table, schema):
"""Compare a database table with the reference model and make necessary changes
This is very basic and only the needed features have been implemented (adding columns)
Args:
table (str): Name of the table to migrate
schema (dict): Reference schema for the table
Returns:
list: The list of column names that have been added
"""
existing_schema = get_schema(table)
migrated_fields = []
if existing_schema:
columns = [col["name"] for col in existing_schema]
for field in schema:
if field["name"] not in columns:
logger.info("Migrating %s field %s", table, field["name"])
migrated_fields.append(field["name"])
sql.add_field(settings.PGA_DB, table, field)
else:
create_table(table, schema)
return migrated_fields
syncdb()
¶
Update the database to the current version, making necessary changes for backwards compatibility.
Source code in lutris/database/schema.py
def syncdb():
"""Update the database to the current version, making necessary changes
for backwards compatibility."""
for table_name, table_data in DATABASE.items():
migrate(table_name, table_data)
services
¶
ServiceGameCollection
¶
Source code in lutris/database/services.py
class ServiceGameCollection:
@classmethod
def get_for_service(cls, service):
if not service:
raise ValueError("No service provided")
return sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service})
@classmethod
def get_game(cls, service, appid):
"""Return a single game refered by its appid"""
if not service:
raise ValueError("No service provided")
if not appid:
raise ValueError("No appid provided")
results = sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service, "appid": appid})
if not results:
return
if len(results) > 1:
logger.warning("More than one game found for %s on %s", appid, service)
return results[0]
get_for_service(service)
classmethod
¶
Source code in lutris/database/services.py
@classmethod
def get_for_service(cls, service):
if not service:
raise ValueError("No service provided")
return sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service})
get_game(service, appid)
classmethod
¶
Return a single game refered by its appid
Source code in lutris/database/services.py
@classmethod
def get_game(cls, service, appid):
"""Return a single game refered by its appid"""
if not service:
raise ValueError("No service provided")
if not appid:
raise ValueError("No appid provided")
results = sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service, "appid": appid})
if not results:
return
if len(results) > 1:
logger.warning("More than one game found for %s on %s", appid, service)
return results[0]
sources
¶
add_source(uri)
¶
Source code in lutris/database/sources.py
def add_source(uri):
sql.db_insert(settings.PGA_DB, "sources", {"uri": uri})
check_for_file(game, file_id)
¶
Source code in lutris/database/sources.py
def check_for_file(game, file_id):
for source in read_sources():
if source.startswith("file://"):
source = source[7:]
else:
protocol = source[:7]
logger.warning("PGA source protocol %s not implemented", protocol)
continue
if not system.path_exists(source):
logger.info("PGA source %s unavailable", source)
continue
game_dir = os.path.join(source, game)
if not system.path_exists(game_dir):
continue
for game_file in os.listdir(game_dir):
game_base, _ext = os.path.splitext(game_file)
if game_base == file_id:
return os.path.join(game_dir, game_file)
return False
delete_source(uri)
¶
Source code in lutris/database/sources.py
def delete_source(uri):
sql.db_delete(settings.PGA_DB, "sources", "uri", uri)
read_sources()
¶
Source code in lutris/database/sources.py
def read_sources():
with sql.db_cursor(settings.PGA_DB) as cursor:
rows = cursor.execute("select uri from sources")
results = rows.fetchall()
return [row[0] for row in results]
write_sources(sources)
¶
Source code in lutris/database/sources.py
def write_sources(sources):
db_sources = read_sources()
for uri in db_sources:
if uri not in sources:
sql.db_delete(settings.PGA_DB, "sources", "uri", uri)
for uri in sources:
if uri not in db_sources:
sql.db_insert(settings.PGA_DB, "sources", {"uri": uri})
sql
¶
DB_LOCK
¶
db_cursor
¶
Source code in lutris/database/sql.py
class db_cursor(object):
def __init__(self, db_path):
self.db_path = db_path
self.db_conn = None
def __enter__(self):
self.db_conn = sqlite3.connect(self.db_path)
cursor = self.db_conn.cursor()
return cursor
def __exit__(self, _type, value, traceback):
self.db_conn.commit()
self.db_conn.close()
__enter__(self)
special
¶
Source code in lutris/database/sql.py
def __enter__(self):
self.db_conn = sqlite3.connect(self.db_path)
cursor = self.db_conn.cursor()
return cursor
__exit__(self, _type, value, traceback)
special
¶
Source code in lutris/database/sql.py
def __exit__(self, _type, value, traceback):
self.db_conn.commit()
self.db_conn.close()
__init__(self, db_path)
special
¶
Source code in lutris/database/sql.py
def __init__(self, db_path):
self.db_path = db_path
self.db_conn = None
add_field(db_path, tablename, field)
¶
Source code in lutris/database/sql.py
def add_field(db_path, tablename, field):
query = "ALTER TABLE %s ADD COLUMN %s %s" % (
tablename,
field["name"],
field["type"],
)
with db_cursor(db_path) as cursor:
cursor.execute(query)
cursor_execute(cursor, query, params=None)
¶
Execute a SQL query, run it in a lock block
Source code in lutris/database/sql.py
def cursor_execute(cursor, query, params=None):
"""Execute a SQL query, run it in a lock block"""
params = params or ()
lock = DB_LOCK.acquire(timeout=1) # pylint: disable=consider-using-with
if not lock:
logger.error("Database is busy. Not executing %s", query)
return
results = cursor.execute(query, params)
DB_LOCK.release()
return results
db_delete(db_path, table, field, value)
¶
Source code in lutris/database/sql.py
def db_delete(db_path, table, field, value):
with db_cursor(db_path) as cursor:
cursor_execute(cursor, "delete from {0} where {1}=?".format(table, field), (value, ))
db_insert(db_path, table, fields)
¶
Source code in lutris/database/sql.py
def db_insert(db_path, table, fields):
columns = ", ".join(list(fields.keys()))
placeholders = ("?, " * len(fields))[:-2]
field_values = tuple(fields.values())
with db_cursor(db_path) as cursor:
cursor_execute(
cursor,
"insert into {0}({1}) values ({2})".format(table, columns, placeholders),
field_values,
)
inserted_id = cursor.lastrowid
return inserted_id
db_query(db_path, query, params=())
¶
Source code in lutris/database/sql.py
def db_query(db_path, query, params=()):
with db_cursor(db_path) as cursor:
cursor_execute(cursor, query, params)
rows = cursor.fetchall()
column_names = [column[0] for column in cursor.description]
results = []
for row in rows:
row_data = {}
for index, column in enumerate(column_names):
row_data[column] = row[index]
results.append(row_data)
return results
db_select(db_path, table, fields=None, condition=None)
¶
Source code in lutris/database/sql.py
def db_select(db_path, table, fields=None, condition=None):
if fields:
columns = ", ".join(fields)
else:
columns = "*"
with db_cursor(db_path) as cursor:
query = "SELECT {} FROM {}"
if condition:
condition_field, condition_value = condition
if isinstance(condition_value, (list, tuple, set)):
condition_value = tuple(condition_value)
placeholders = ", ".join("?" * len(condition_value))
where_condition = " where {} in (" + placeholders + ")"
else:
condition_value = (condition_value, )
where_condition = " where {}=?"
query = query + where_condition
query = query.format(columns, table, condition_field)
params = condition_value
else:
query = query.format(columns, table)
params = ()
cursor_execute(cursor, query, params)
rows = cursor.fetchall()
column_names = [column[0] for column in cursor.description]
results = []
for row in rows:
row_data = {}
for index, column in enumerate(column_names):
row_data[column] = row[index]
results.append(row_data)
return results
db_update(db_path, table, updated_fields, conditions)
¶
Update table with the values given in the dict values on the
condition given with the row tuple.
Source code in lutris/database/sql.py
def db_update(db_path, table, updated_fields, conditions):
"""Update `table` with the values given in the dict `values` on the
condition given with the `row` tuple.
"""
columns = "=?, ".join(list(updated_fields.keys())) + "=?"
field_values = tuple(updated_fields.values())
condition_field = " AND ".join(["%s=?" % field for field in conditions])
condition_value = tuple(conditions.values())
with db_cursor(db_path) as cursor:
query = "UPDATE {0} SET {1} WHERE {2}".format(table, columns, condition_field)
result = cursor_execute(cursor, query, field_values + condition_value)
return result
filtered_query(db_path, table, searches=None, filters=None, excludes=None, sorts=None)
¶
Source code in lutris/database/sql.py
def filtered_query(
db_path,
table,
searches=None,
filters=None,
excludes=None,
sorts=None
):
query = "select * from %s" % table
params = []
sql_filters = []
for field in searches or {}:
sql_filters.append("%s LIKE ?" % field)
params.append("%" + searches[field] + "%")
for field in filters or {}:
if filters[field] is not None: # but 0 or False are okay!
sql_filters.append("%s = ?" % field)
params.append(filters[field])
for field in excludes or {}:
if excludes[field]:
sql_filters.append("%s IS NOT ?" % field)
params.append(excludes[field])
if sql_filters:
query += " WHERE " + " AND ".join(sql_filters)
if sorts:
query += " ORDER BY %s" % ", ".join(
["%s %s" % (sort[0], sort[1]) for sort in sorts]
)
else:
query += " ORDER BY slug ASC"
return db_query(db_path, query, tuple(params))
discord
¶
Discord integration
PyPresence
¶
PyPresenceException
¶
DiscordPresence
¶
Provide rich presence integration with Discord for games
Source code in lutris/discord.py
class DiscordPresence(object):
"""Provide rich presence integration with Discord for games"""
def __init__(self):
self.available = bool(PyPresence)
self.game_name = ""
self.runner_name = ""
self.last_rpc = 0
self.rpc_interval = 60
self.presence_connected = False
self.rpc_client = None
self.client_id = None
def connect(self):
"""Make sure we are actually connected before trying to send requests"""
if not self.presence_connected:
self.rpc_client = PyPresence(self.client_id)
try:
self.rpc_client.connect()
self.presence_connected = True
except (ConnectionError, FileNotFoundError):
logger.error("Could not connect to Discord")
return self.presence_connected
def disconnect(self):
"""Ensure we are definitely disconnected and fix broken event loop from pypresence
That method is a huge mess of non-deterministic bs and should be nuked from orbit.
"""
if self.rpc_client:
try:
self.rpc_client.close()
except Exception as e:
logger.exception("Unable to close Discord RPC connection: %s", e)
if self.rpc_client.sock_writer is not None:
try:
self.rpc_client.sock_writer.close()
except Exception:
logger.exception("Sock writer could not be closed.")
try:
logger.debug("Forcefully closing event loop.")
self.rpc_client.loop.close()
except Exception:
logger.debug("Could not close event loop.")
try:
logger.debug("Forcefully replacing event loop.")
self.rpc_client.loop = None
asyncio.set_event_loop(asyncio.new_event_loop())
except Exception as e:
logger.exception("Could not replace event loop: %s", e)
try:
logger.debug("Forcefully deleting RPC client.")
self.rpc_client = None
except Exception as ex:
logger.exception(ex)
self.rpc_client = None
self.presence_connected = False
def update_discord_rich_presence(self):
"""Dispatch a request to Discord to update presence"""
if int(time.time()) - self.rpc_interval < self.last_rpc:
logger.debug("Not enough time since last RPC")
return
self.last_rpc = int(time.time())
if not self.connect():
return
try:
self.rpc_client.update(details="Playing %s" % self.game_name,
large_image="large_image",
large_text=self.game_name,
small_image="small_image")
except PyPresenceException as ex:
logger.error("Unable to update Discord: %s", ex)
def clear_discord_rich_presence(self):
"""Dispatch a request to Discord to clear presence"""
if self.connect():
try:
self.rpc_client.clear()
except PyPresenceException as ex:
logger.error("Unable to clear Discord: %s", ex)
self.disconnect()
__init__(self)
special
¶
Source code in lutris/discord.py
def __init__(self):
self.available = bool(PyPresence)
self.game_name = ""
self.runner_name = ""
self.last_rpc = 0
self.rpc_interval = 60
self.presence_connected = False
self.rpc_client = None
self.client_id = None
clear_discord_rich_presence(self)
¶
Dispatch a request to Discord to clear presence
Source code in lutris/discord.py
def clear_discord_rich_presence(self):
"""Dispatch a request to Discord to clear presence"""
if self.connect():
try:
self.rpc_client.clear()
except PyPresenceException as ex:
logger.error("Unable to clear Discord: %s", ex)
self.disconnect()
connect(self)
¶
Make sure we are actually connected before trying to send requests
Source code in lutris/discord.py
def connect(self):
"""Make sure we are actually connected before trying to send requests"""
if not self.presence_connected:
self.rpc_client = PyPresence(self.client_id)
try:
self.rpc_client.connect()
self.presence_connected = True
except (ConnectionError, FileNotFoundError):
logger.error("Could not connect to Discord")
return self.presence_connected
disconnect(self)
¶
Ensure we are definitely disconnected and fix broken event loop from pypresence That method is a huge mess of non-deterministic bs and should be nuked from orbit.
Source code in lutris/discord.py
def disconnect(self):
"""Ensure we are definitely disconnected and fix broken event loop from pypresence
That method is a huge mess of non-deterministic bs and should be nuked from orbit.
"""
if self.rpc_client:
try:
self.rpc_client.close()
except Exception as e:
logger.exception("Unable to close Discord RPC connection: %s", e)
if self.rpc_client.sock_writer is not None:
try:
self.rpc_client.sock_writer.close()
except Exception:
logger.exception("Sock writer could not be closed.")
try:
logger.debug("Forcefully closing event loop.")
self.rpc_client.loop.close()
except Exception:
logger.debug("Could not close event loop.")
try:
logger.debug("Forcefully replacing event loop.")
self.rpc_client.loop = None
asyncio.set_event_loop(asyncio.new_event_loop())
except Exception as e:
logger.exception("Could not replace event loop: %s", e)
try:
logger.debug("Forcefully deleting RPC client.")
self.rpc_client = None
except Exception as ex:
logger.exception(ex)
self.rpc_client = None
self.presence_connected = False
update_discord_rich_presence(self)
¶
Dispatch a request to Discord to update presence
Source code in lutris/discord.py
def update_discord_rich_presence(self):
"""Dispatch a request to Discord to update presence"""
if int(time.time()) - self.rpc_interval < self.last_rpc:
logger.debug("Not enough time since last RPC")
return
self.last_rpc = int(time.time())
if not self.connect():
return
try:
self.rpc_client.update(details="Playing %s" % self.game_name,
large_image="large_image",
large_text=self.game_name,
small_image="small_image")
except PyPresenceException as ex:
logger.error("Unable to update Discord: %s", ex)
exceptions
¶
Exception handling module
AuthenticationError (Exception)
¶
Raised when authentication to a service fails
Source code in lutris/exceptions.py
class AuthenticationError(Exception):
"""Raised when authentication to a service fails"""
GameConfigError (LutrisError)
¶
Throw this error when the game configuration prevents the game from running properly.
Source code in lutris/exceptions.py
class GameConfigError(LutrisError):
"""Throw this error when the game configuration prevents the game from
running properly.
"""
LutrisError (Exception)
¶
Base exception for Lutris related errors
Source code in lutris/exceptions.py
class LutrisError(Exception):
"""Base exception for Lutris related errors"""
def __init__(self, message):
super().__init__(message)
self.message = message
__init__(self, message)
special
¶
Source code in lutris/exceptions.py
def __init__(self, message):
super().__init__(message)
self.message = message
MultipleInstallerError (BaseException)
¶
Current implementation doesn't know how to deal with multiple installers Raise this if a game returns more than 1 installer.
Source code in lutris/exceptions.py
class MultipleInstallerError(BaseException):
"""Current implementation doesn't know how to deal with multiple installers
Raise this if a game returns more than 1 installer."""
UnavailableGame (Exception)
¶
Raised when a game is available from a service
Source code in lutris/exceptions.py
class UnavailableGame(Exception):
"""Raised when a game is available from a service"""
UnavailableLibraries (RuntimeError)
¶
Source code in lutris/exceptions.py
class UnavailableLibraries(RuntimeError):
def __init__(self, libraries, arch=None):
message = _(
"The following {arch} libraries are required but are not installed on your system:\n{libs}"
).format(
arch=arch if arch else "",
libs=", ".join(libraries)
)
super().__init__(message)
self.libraries = libraries
__init__(self, libraries, arch=None)
special
¶
Source code in lutris/exceptions.py
def __init__(self, libraries, arch=None):
message = _(
"The following {arch} libraries are required but are not installed on your system:\n{libs}"
).format(
arch=arch if arch else "",
libs=", ".join(libraries)
)
super().__init__(message)
self.libraries = libraries
watch_lutris_errors(function)
¶
Decorator used to catch LutrisError exceptions and send events
Source code in lutris/exceptions.py
def watch_lutris_errors(function):
"""Decorator used to catch LutrisError exceptions and send events"""
@wraps(function)
def wrapper(*args, **kwargs):
"""Catch all LutrisError exceptions and emit an event."""
try:
return function(*args, **kwargs)
except LutrisError as ex:
game = args[0]
game.emit("game-error", ex.message)
return wrapper
game
¶
Module that actually runs the games.
HEARTBEAT_DELAY
¶
Game (Object)
¶
This class takes cares of loading the configuration for a game and running it.
Source code in lutris/game.py
class Game(GObject.Object):
"""This class takes cares of loading the configuration for a game
and running it.
"""
now_playing_path = os.path.join(settings.CACHE_DIR, "now-playing.txt")
STATE_STOPPED = "stopped"
STATE_LAUNCHING = "launching"
STATE_RUNNING = "running"
__gsignals__ = {
"game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
"game-launch": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-start": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-started": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-stopped": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-install": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-install-update": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-install-dlc": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, game_id=None):
super().__init__()
self.id = game_id # pylint: disable=invalid-name
self.runner = None
self.config = None
# Load attributes from database
game_data = games_db.get_game_by_field(game_id, "id")
self.slug = game_data.get("slug") or ""
self.runner_name = game_data.get("runner") or ""
self.directory = game_data.get("directory") or ""
self.name = game_data.get("name") or ""
self.game_config_id = game_data.get("configpath") or ""
self.is_installed = bool(game_data.get("installed") and self.game_config_id)
self.is_hidden = bool(game_data.get("hidden"))
self.platform = game_data.get("platform") or ""
self.year = game_data.get("year") or ""
self.lastplayed = game_data.get("lastplayed") or 0
self.has_custom_banner = bool(game_data.get("has_custom_banner"))
self.has_custom_icon = bool(game_data.get("has_custom_icon"))
self.service = game_data.get("service")
self.appid = game_data.get("service_id")
self.playtime = game_data.get("playtime") or 0.0
if self.game_config_id:
self.load_config()
self.game_uuid = None
self.game_thread = None
self.antimicro_thread = None
self.prelaunch_pids = []
self.prelaunch_executor = None
self.heartbeat = None
self.killswitch = None
self.state = self.STATE_STOPPED
self.game_runtime_config = {}
self.resolution_changed = False
self.compositor_disabled = False
self.original_outputs = None
self._log_buffer = None
self.timer = Timer()
self.screen_saver_inhibitor_cookie = None
def __repr__(self):
return self.__str__()
def __str__(self):
value = self.name or "Game (no name)"
if self.runner_name:
value += " (%s)" % self.runner_name
return value
@property
def is_updatable(self):
"""Return whether the game can be upgraded"""
return self.service == "gog"
@property
def is_favorite(self):
"""Return whether the game is in the user's favorites"""
categories = categories_db.get_categories_in_game(self.id)
for category in categories:
if category == "favorite":
return True
return False
def add_to_favorites(self):
"""Add the game to the 'favorite' category"""
favorite = categories_db.get_category("favorite")
if not favorite:
favorite = categories_db.add_category("favorite")
categories_db.add_game_to_category(self.id, favorite["id"])
self.emit("game-updated")
def remove_from_favorites(self):
"""Remove game from favorites"""
favorite = categories_db.get_category("favorite")
categories_db.remove_category_from_game(self.id, favorite["id"])
self.emit("game-updated")
def set_hidden(self, is_hidden):
"""Do not show this game in the UI"""
self.is_hidden = is_hidden
self.save()
self.emit("game-updated")
@property
def log_buffer(self):
"""Access the log buffer object, creating it if necessary"""
_log_buffer = LOG_BUFFERS.get(str(self.id))
if _log_buffer:
return _log_buffer
_log_buffer = Gtk.TextBuffer()
_log_buffer.create_tag("warning", foreground="red")
if self.game_thread:
self.game_thread.set_log_buffer(self._log_buffer)
_log_buffer.set_text(self.game_thread.stdout)
LOG_BUFFERS[str(self.id)] = _log_buffer
return _log_buffer
@property
def formatted_playtime(self):
"""Return a human readable formatted play time"""
return strings.get_formatted_playtime(self.playtime)
@staticmethod
def show_error_message(message):
"""Display an error message based on the runner's output."""
if message["error"] == "CUSTOM":
message_text = message["text"].replace("&", "&")
dialogs.ErrorDialog(message_text)
elif message["error"] == "RUNNER_NOT_INSTALLED":
dialogs.ErrorDialog(_("Error the runner is not installed"))
elif message["error"] == "NO_BIOS":
dialogs.ErrorDialog(_("A bios file is required to run this game"))
elif message["error"] == "FILE_NOT_FOUND":
filename = message["file"]
if filename:
message_text = _("The file {} could not be found").format(filename.replace("&", "&"))
else:
message_text = _("This game has no executable set. The install process didn't finish properly.")
dialogs.ErrorDialog(message_text)
elif message["error"] == "NOT_EXECUTABLE":
message_text = message["file"].replace("&", "&")
dialogs.ErrorDialog(_("The file %s is not executable") % message_text)
elif message["error"] == "PATH_NOT_SET":
message_text = _("The path '%s' is not set. please set it in the options.") % message["path"]
dialogs.ErrorDialog(message_text)
else:
dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"])
def get_browse_dir(self):
"""Return the path to open with the Browse Files action."""
return self.runner.game_path
def _get_runner(self):
"""Return the runner instance for this game's configuration"""
try:
runner_class = import_runner(self.runner_name)
return runner_class(self.config)
except InvalidRunner:
logger.error("Unable to import runner %s for %s", self.runner_name, self.slug)
def load_config(self):
"""Load the game's configuration."""
if not self.is_installed:
return
self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id)
self.runner = self._get_runner()
def set_desktop_compositing(self, enable):
"""Enables or disables compositing"""
if enable:
if self.compositor_disabled:
enable_compositing()
self.compositor_disabled = False
else:
if not self.compositor_disabled:
disable_compositing()
self.compositor_disabled = True
def remove(self, delete_files=False, no_signal=False):
"""Uninstall a game
Params:
delete_files (bool): Delete the game files
no_signal (bool): Don't emit game-removed signal (if running in a thread)
"""
sql.db_update(settings.PGA_DB, "games", {"installed": 0, "runner": ""}, {"id": self.id})
if self.config:
self.config.remove()
xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True)
if delete_files and self.runner:
self.runner.remove_game_data(game_path=self.directory)
self.is_installed = False
self.runner = None
if no_signal:
return
self.emit("game-removed")
def delete(self):
"""Completely remove a game from the library"""
if self.is_installed:
raise RuntimeError("Uninstall the game before deleting")
games_db.delete_game(self.id)
self.emit("game-removed")
def set_platform_from_runner(self):
"""Set the game's platform from the runner"""
if not self.runner:
logger.warning("Game has no runner, can't set platform")
return
self.platform = self.runner.get_platform()
if not self.platform:
logger.warning("The %s runner didn't provide a platform for %s", self.runner.human_name, self)
def save(self, save_config=False):
"""
Save the game's config and metadata, if `save_config` is set to False,
do not save the config. This is useful when exiting the game since the
config might have changed and we don't want to override the changes.
"""
if self.config:
logger.debug("Saving %s with config ID %s", self, self.config.game_config_id)
configpath = self.config.game_config_id
if save_config:
self.config.save()
else:
logger.warning("Saving %s without a configuration", self)
configpath = ""
self.set_platform_from_runner()
self.id = games_db.add_or_update(
name=self.name,
runner=self.runner_name,
slug=self.slug,
platform=self.platform,
directory=self.directory,
installed=self.is_installed,
year=self.year,
lastplayed=self.lastplayed,
configpath=configpath,
id=self.id,
playtime=self.playtime,
hidden=self.is_hidden,
service=self.service,
service_id=self.appid,
)
self.emit("game-updated")
def is_launchable(self):
"""Verify that the current game can be launched."""
if not self.is_installed:
logger.error("%s (%s) not installed", self, self.id)
dialogs.ErrorDialog(_("Tried to launch a game that isn't installed."))
return False
if not self.runner:
dialogs.ErrorDialog(_("Invalid game configuration: Missing runner"))
return False
if not self.runner.is_installed():
installed = self.runner.install_dialog()
if not installed:
dialogs.ErrorDialog(_("Runner not installed."))
return False
if self.runner.use_runtime():
runtime_updater = runtime.RuntimeUpdater()
if runtime_updater.is_updating():
dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected"))
if ("wine" in self.runner_name and not wine.get_wine_version() and not LINUX_SYSTEM.is_flatpak):
dialogs.WineNotInstalledWarning(parent=None)
return True
def restrict_to_display(self, display):
outputs = DISPLAY_MANAGER.get_config()
if display == "primary":
display = None
for output in outputs:
if output.primary:
display = output.name
break
if not display:
logger.warning("No primary display set")
else:
found = False
for output in outputs:
if output.name == display:
found = True
break
if not found:
logger.warning("Selected display %s not found", display)
display = None
if display:
turn_off_except(display)
time.sleep(3)
return True
return False
def start_xephyr(self, display=":2"):
"""Start a monitored Xephyr instance"""
if not system.find_executable("Xephyr"):
raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option")
xephyr_command = get_xephyr_command(display, self.runner.system_config)
xephyr_thread = MonitoredCommand(xephyr_command)
xephyr_thread.start()
time.sleep(3)
return display
def start_antimicrox(self, antimicro_config):
"""Start Antimicrox with a given config path"""
antimicro_path = system.find_executable("antimicrox")
if not antimicro_path:
logger.warning("Antimicrox is not installed.")
return
logger.info("Starting Antic")
antimicro_command = [antimicro_path, "--hidden", "--tray", "--profile", antimicro_config]
self.antimicro_thread = MonitoredCommand(antimicro_command)
self.antimicro_thread.start()
@staticmethod
def set_keyboard_layout(layout):
setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"]
xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")]
with subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) as xkbcomp:
with subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin) as setxkbmap:
setxkbmap.communicate()
xkbcomp.communicate()
def start_prelaunch_command(self, wait_for_completion=False):
"""Start the prelaunch command specified in the system options"""
prelaunch_command = self.runner.system_config.get("prelaunch_command")
command_array = shlex.split(prelaunch_command)
if not system.path_exists(command_array[0]):
logger.warning("Command %s not found", command_array[0])
return
env = self.game_runtime_config["env"]
if wait_for_completion:
logger.info("Prelauch command: %s, waiting for completion", prelaunch_command)
# Monitor the prelaunch command and wait until it has finished
system.execute(command_array, env=env, cwd=self.directory)
else:
logger.info("Prelaunch command %s launched in the background", prelaunch_command)
self.prelaunch_executor = MonitoredCommand(
command_array,
include_processes=[os.path.basename(command_array[0])],
env=env,
cwd=self.directory,
)
self.prelaunch_executor.start()
def get_terminal(self):
"""Return the terminal used to run the game into or None if the game is not run from a terminal.
Remember that only games using text mode should use the terminal.
"""
if self.runner.system_config.get("terminal"):
terminal = self.runner.system_config.get("terminal_app", linux.get_default_terminal())
if terminal and not system.find_executable(terminal):
raise GameConfigError(_("The selected terminal application could not be launched:\n%s") % terminal)
return terminal
def get_killswitch(self):
"""Return the path to a file that is monitored during game execution.
If the file stops existing, the game is stopped.
"""
killswitch = self.runner.system_config.get("killswitch")
# Prevent setting a killswitch to a file that doesn't exists
if killswitch and system.path_exists(self.killswitch):
return killswitch
def get_gameplay_info(self):
"""Return the information provided by a runner's play method.
Checks for possible errors.
"""
if not self.runner:
logger.warning("Trying to launch %s without a runner", self)
return {}
gameplay_info = self.runner.play()
if "error" in gameplay_info:
self.show_error_message(gameplay_info)
self.state = self.STATE_STOPPED
self.emit("game-stop")
return
if self.config.game_level.get("game", {}).get("launch_configs"):
configs = self.config.game_level["game"]["launch_configs"]
dlg = dialogs.LaunchConfigSelectDialog(self, configs)
if dlg.config_index:
config = configs[dlg.config_index - 1]
if "command" not in gameplay_info:
logger.debug("No command in %s", gameplay_info)
logger.debug(config)
return {}
gameplay_info["command"] = [gameplay_info["command"][0], config["exe"]]
if config.get("args"):
gameplay_info["command"] += strings.split_arguments(config["args"])
return gameplay_info
@watch_lutris_errors
def configure_game(self, prelaunched, error=None): # noqa: C901
"""Get the game ready to start, applying all the options
This methods sets the game_runtime_config attribute.
"""
if error:
logger.error(error)
dialogs.ErrorDialog(str(error))
if not prelaunched:
logger.error("Game prelaunch unsuccessful")
dialogs.ErrorDialog(_("An error prevented the game from running"))
self.state = self.STATE_STOPPED
self.emit("game-stop")
return
gameplay_info = self.get_gameplay_info()
if not gameplay_info:
return
command, env = get_launch_parameters(self.runner, gameplay_info)
env["game_name"] = self.name # What is this used for??
self.game_runtime_config = {
"args": command,
"env": env,
"terminal": self.get_terminal(),
"include_processes": shlex.split(self.runner.system_config.get("include_processes", "")),
"exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")),
}
# Audio control
if self.runner.system_config.get("reset_pulse"):
audio.reset_pulse()
# Input control
if self.runner.system_config.get("use_us_layout"):
self.set_keyboard_layout("us")
# Display control
self.original_outputs = DISPLAY_MANAGER.get_config()
if self.runner.system_config.get("disable_compositor"):
self.set_desktop_compositing(False)
if self.runner.system_config.get("disable_screen_saver"):
self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit(self.name)
if self.runner.system_config.get("display") != "off":
self.resolution_changed = self.restrict_to_display(self.runner.system_config.get("display"))
resolution = self.runner.system_config.get("resolution")
if resolution != "off":
DISPLAY_MANAGER.set_resolution(resolution)
time.sleep(3)
self.resolution_changed = True
xephyr = self.runner.system_config.get("xephyr") or "off"
if xephyr != "off":
env["DISPLAY"] = self.start_xephyr()
antimicro_config = self.runner.system_config.get("antimicro_config")
if system.path_exists(antimicro_config):
self.start_antimicrox(antimicro_config)
# Execution control
self.killswitch = self.get_killswitch()
if self.runner.system_config.get("prelaunch_command"):
self.start_prelaunch_command(self.runner.system_config.get("prelaunch_wait"))
self.start_game()
def launch(self):
"""Request launching a game. The game may not be installed yet."""
if not self.is_launchable():
logger.error("Game is not launchable")
return
self.load_config() # Reload the config before launching it.
if str(self.id) in LOG_BUFFERS: # Reset game logs on each launch
log_buffer = LOG_BUFFERS[str(self.id)]
log_buffer.delete(log_buffer.get_start_iter(), log_buffer.get_end_iter())
self.state = self.STATE_LAUNCHING
self.prelaunch_pids = system.get_running_pid_list()
self.emit("game-start")
jobs.AsyncCall(self.runner.prelaunch, self.configure_game)
def start_game(self):
"""Run a background command to lauch the game"""
self.game_thread = MonitoredCommand(
self.game_runtime_config["args"],
title=self.name,
runner=self.runner,
env=self.game_runtime_config["env"],
term=self.game_runtime_config["terminal"],
log_buffer=self.log_buffer,
include_processes=self.game_runtime_config["include_processes"],
exclude_processes=self.game_runtime_config["exclude_processes"],
)
if hasattr(self.runner, "stop"):
self.game_thread.stop_func = self.runner.stop
self.game_uuid = self.game_thread.env["LUTRIS_GAME_UUID"]
self.game_thread.start()
self.timer.start()
self.state = self.STATE_RUNNING
self.emit("game-started")
self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat)
with open(self.now_playing_path, "w", encoding="utf-8") as np_file:
np_file.write(self.name)
def force_stop(self):
# If force_stop_game fails, wait a few seconds and try SIGKILL on any survivors
self.runner.force_stop_game(self)
if self.get_stop_pids():
self.force_kill_delayed()
else:
self.stop_game()
def force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=.5):
"""Forces termination of a running game, but only after a set time has elapsed;
Invokes stop_game() when the game is dead."""
def death_watch():
"""Wait for the processes to die; returns True if do they all did."""
for _n in range(int(death_watch_seconds / death_watch_interval_seconds)):
time.sleep(death_watch_interval_seconds)
if not self.get_stop_pids():
return True
return False
def death_watch_cb(all_died, error):
"""Called after the death watch to more firmly kill any survivors."""
if error:
dialogs.ErrorDialog(str(error))
elif not all_died:
self.kill_processes(signal.SIGKILL)
# If we still can't kill everything, we'll still say we stopped it.
self.stop_game()
jobs.AsyncCall(death_watch, death_watch_cb)
def kill_processes(self, sig):
"""Sends a signal to a process list, logging errors."""
pids = self.get_stop_pids()
for pid in pids:
try:
os.kill(int(pid), sig)
except ProcessLookupError as ex:
logger.debug("Failed to kill game process: %s", ex)
def get_stop_pids(self):
"""Finds the PIDs of processes that need killin'!"""
pids = self.get_game_pids()
if self.game_thread and self.game_thread.game_process:
pids.add(self.game_thread.game_process.pid)
return pids
def get_game_pids(self):
"""Return a list of processes belonging to the Lutris game"""
new_pids = self.get_new_pids()
game_pids = []
game_folder = self.runner.game_path or ""
for pid in new_pids:
cmdline = Process(pid).cmdline or ""
# pressure-vessel: This could potentially pick up PIDs not started by lutris?
if game_folder in cmdline or "pressure-vessel" in cmdline:
game_pids.append(pid)
return set(game_pids + [
pid for pid in new_pids
if Process(pid).environ.get("LUTRIS_GAME_UUID") == self.game_uuid
])
def get_new_pids(self):
"""Return list of PIDs started since the game was launched"""
return set(system.get_running_pid_list()) - set(self.prelaunch_pids)
def stop_game(self):
"""Cleanup after a game as stopped"""
duration = self.timer.duration
logger.debug("%s has run for %s seconds", self, duration)
if duration < 5:
logger.warning("The game has run for a very short time, did it crash?")
# Inspect why it could have crashed
self.state = self.STATE_STOPPED
self.emit("game-stop")
if os.path.exists(self.now_playing_path):
os.unlink(self.now_playing_path)
if not self.timer.finished:
self.timer.end()
self.playtime += self.timer.duration / 3600
def prelaunch_beat(self):
"""Watch the prelaunch command"""
if self.prelaunch_executor and self.prelaunch_executor.is_running:
return True
self.start_game()
return False
def beat(self):
"""Watch the game's process(es)."""
if self.game_thread.error:
dialogs.ErrorDialog(_("<b>Error lauching the game:</b>\n") + self.game_thread.error)
self.on_game_quit()
return False
# The killswitch file should be set to a device (ie. /dev/input/js0)
# When that device is unplugged, the game is forced to quit.
killswitch_engage = self.killswitch and not system.path_exists(self.killswitch)
if killswitch_engage:
logger.warning("File descriptor no longer present, force quit the game")
self.force_stop()
return False
game_pids = self.get_game_pids()
if not self.game_thread.is_running and not game_pids:
logger.debug("Game thread stopped")
self.on_game_quit()
return False
return True
def stop(self):
"""Stops the game"""
if self.state == self.STATE_STOPPED:
logger.debug("Game already stopped")
return
logger.info("Stopping %s", self)
if self.game_thread:
jobs.AsyncCall(self.game_thread.stop, None)
self.stop_game()
def on_game_quit(self):
"""Restore some settings and cleanup after game quit."""
if self.prelaunch_executor and self.prelaunch_executor.is_running:
logger.info("Stopping prelaunch script")
self.prelaunch_executor.stop()
self.heartbeat = None
if self.state != self.STATE_STOPPED:
logger.warning("Game still running (state: %s)", self.state)
self.stop()
# Check for post game script
postexit_command = self.runner.system_config.get("postexit_command")
if postexit_command:
command_array = shlex.split(postexit_command)
if system.path_exists(command_array[0]):
logger.info("Running post-exit command: %s", postexit_command)
postexit_thread = MonitoredCommand(
command_array,
include_processes=[os.path.basename(postexit_command)],
env=self.game_runtime_config["env"],
cwd=self.directory,
)
postexit_thread.start()
quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
logger.debug("%s stopped at %s", self.name, quit_time)
self.lastplayed = int(time.time())
self.save(save_config=False)
os.chdir(os.path.expanduser("~"))
if self.antimicro_thread:
self.antimicro_thread.stop()
if self.resolution_changed or self.runner.system_config.get("reset_desktop"):
DISPLAY_MANAGER.set_resolution(self.original_outputs)
if self.compositor_disabled:
self.set_desktop_compositing(True)
if self.screen_saver_inhibitor_cookie is not None:
SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie)
self.screen_saver_inhibitor_cookie = None
if self.runner.system_config.get("use_us_layout"):
with subprocess.Popen(["setxkbmap"], env=os.environ) as setxkbmap:
setxkbmap.communicate()
if self.runner.system_config.get("restore_gamma"):
restore_gamma()
self.process_return_codes()
def process_return_codes(self):
"""Do things depending on how the game quitted."""
if self.game_thread.return_code == 127:
# Error missing shared lib
error = "error while loading shared lib"
error_line = strings.lookup_string_in_text(error, self.game_thread.stdout)
if error_line:
dialogs.ErrorDialog(_("<b>Error: Missing shared library.</b>\n\n%s") % error_line)
if self.game_thread.return_code == 1:
# Error Wine version conflict
error = "maybe the wrong wineserver"
if strings.lookup_string_in_text(error, self.game_thread.stdout):
dialogs.ErrorDialog(_("<b>Error: A different Wine version is already using the same Wine prefix.</b>"))
def write_script(self, script_path):
"""Output the launch argument in a bash script"""
gameplay_info = self.get_gameplay_info()
if not gameplay_info:
logger.error("Unable to retrieve game information for %s. Can't write a script", self)
return
export_bash_script(self.runner, gameplay_info, script_path)
def move(self, new_location):
logger.info("Moving %s to %s", self, new_location)
new_config = ""
old_location = self.directory
if os.path.exists(old_location):
game_directory = os.path.basename(old_location)
target_directory = os.path.join(new_location, game_directory)
else:
target_directory = new_location
self.directory = target_directory
self.save()
if not old_location:
logger.info("Previous location wasn't set. Cannot continue moving")
return target_directory
with open(self.config.game_config_path, encoding='utf-8') as config_file:
for line in config_file.readlines():
if target_directory in line:
new_config += line
else:
new_config += line.replace(old_location, target_directory)
with open(self.config.game_config_path, "w", encoding='utf-8') as config_file:
config_file.write(new_config)
if not system.path_exists(old_location):
logger.warning("Location %s doesn't exist, files already moved?", old_location)
return target_directory
if new_location.startswith(old_location):
logger.warning("Can't move %s to one of its children %s", old_location, new_location)
return target_directory
try:
shutil.move(old_location, new_location)
except OSError as ex:
logger.error(
"Failed to move %s to %s, you may have to move files manually (Exception: %s)",
old_location, new_location, ex
)
return target_directory
STATE_LAUNCHING
¶
STATE_RUNNING
¶
STATE_STOPPED
¶
formatted_playtime
property
readonly
¶
Return a human readable formatted play time
is_favorite
property
readonly
¶
Return whether the game is in the user's favorites
is_updatable
property
readonly
¶
Return whether the game can be upgraded
log_buffer
property
readonly
¶
Access the log buffer object, creating it if necessary
now_playing_path
¶
__init__(self, game_id=None)
special
¶
Source code in lutris/game.py
def __init__(self, game_id=None):
super().__init__()
self.id = game_id # pylint: disable=invalid-name
self.runner = None
self.config = None
# Load attributes from database
game_data = games_db.get_game_by_field(game_id, "id")
self.slug = game_data.get("slug") or ""
self.runner_name = game_data.get("runner") or ""
self.directory = game_data.get("directory") or ""
self.name = game_data.get("name") or ""
self.game_config_id = game_data.get("configpath") or ""
self.is_installed = bool(game_data.get("installed") and self.game_config_id)
self.is_hidden = bool(game_data.get("hidden"))
self.platform = game_data.get("platform") or ""
self.year = game_data.get("year") or ""
self.lastplayed = game_data.get("lastplayed") or 0
self.has_custom_banner = bool(game_data.get("has_custom_banner"))
self.has_custom_icon = bool(game_data.get("has_custom_icon"))
self.service = game_data.get("service")
self.appid = game_data.get("service_id")
self.playtime = game_data.get("playtime") or 0.0
if self.game_config_id:
self.load_config()
self.game_uuid = None
self.game_thread = None
self.antimicro_thread = None
self.prelaunch_pids = []
self.prelaunch_executor = None
self.heartbeat = None
self.killswitch = None
self.state = self.STATE_STOPPED
self.game_runtime_config = {}
self.resolution_changed = False
self.compositor_disabled = False
self.original_outputs = None
self._log_buffer = None
self.timer = Timer()
self.screen_saver_inhibitor_cookie = None
__repr__(self)
special
¶
Source code in lutris/game.py
def __repr__(self):
return self.__str__()
__str__(self)
special
¶
Source code in lutris/game.py
def __str__(self):
value = self.name or "Game (no name)"
if self.runner_name:
value += " (%s)" % self.runner_name
return value
add_to_favorites(self)
¶
Add the game to the 'favorite' category
Source code in lutris/game.py
def add_to_favorites(self):
"""Add the game to the 'favorite' category"""
favorite = categories_db.get_category("favorite")
if not favorite:
favorite = categories_db.add_category("favorite")
categories_db.add_game_to_category(self.id, favorite["id"])
self.emit("game-updated")
beat(self)
¶
Watch the game's process(es).
Source code in lutris/game.py
def beat(self):
"""Watch the game's process(es)."""
if self.game_thread.error:
dialogs.ErrorDialog(_("<b>Error lauching the game:</b>\n") + self.game_thread.error)
self.on_game_quit()
return False
# The killswitch file should be set to a device (ie. /dev/input/js0)
# When that device is unplugged, the game is forced to quit.
killswitch_engage = self.killswitch and not system.path_exists(self.killswitch)
if killswitch_engage:
logger.warning("File descriptor no longer present, force quit the game")
self.force_stop()
return False
game_pids = self.get_game_pids()
if not self.game_thread.is_running and not game_pids:
logger.debug("Game thread stopped")
self.on_game_quit()
return False
return True
configure_game(self, prelaunched, error=None)
¶
Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute.
Source code in lutris/game.py
@watch_lutris_errors
def configure_game(self, prelaunched, error=None): # noqa: C901
"""Get the game ready to start, applying all the options
This methods sets the game_runtime_config attribute.
"""
if error:
logger.error(error)
dialogs.ErrorDialog(str(error))
if not prelaunched:
logger.error("Game prelaunch unsuccessful")
dialogs.ErrorDialog(_("An error prevented the game from running"))
self.state = self.STATE_STOPPED
self.emit("game-stop")
return
gameplay_info = self.get_gameplay_info()
if not gameplay_info:
return
command, env = get_launch_parameters(self.runner, gameplay_info)
env["game_name"] = self.name # What is this used for??
self.game_runtime_config = {
"args": command,
"env": env,
"terminal": self.get_terminal(),
"include_processes": shlex.split(self.runner.system_config.get("include_processes", "")),
"exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")),
}
# Audio control
if self.runner.system_config.get("reset_pulse"):
audio.reset_pulse()
# Input control
if self.runner.system_config.get("use_us_layout"):
self.set_keyboard_layout("us")
# Display control
self.original_outputs = DISPLAY_MANAGER.get_config()
if self.runner.system_config.get("disable_compositor"):
self.set_desktop_compositing(False)
if self.runner.system_config.get("disable_screen_saver"):
self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit(self.name)
if self.runner.system_config.get("display") != "off":
self.resolution_changed = self.restrict_to_display(self.runner.system_config.get("display"))
resolution = self.runner.system_config.get("resolution")
if resolution != "off":
DISPLAY_MANAGER.set_resolution(resolution)
time.sleep(3)
self.resolution_changed = True
xephyr = self.runner.system_config.get("xephyr") or "off"
if xephyr != "off":
env["DISPLAY"] = self.start_xephyr()
antimicro_config = self.runner.system_config.get("antimicro_config")
if system.path_exists(antimicro_config):
self.start_antimicrox(antimicro_config)
# Execution control
self.killswitch = self.get_killswitch()
if self.runner.system_config.get("prelaunch_command"):
self.start_prelaunch_command(self.runner.system_config.get("prelaunch_wait"))
self.start_game()
delete(self)
¶
Completely remove a game from the library
Source code in lutris/game.py
def delete(self):
"""Completely remove a game from the library"""
if self.is_installed:
raise RuntimeError("Uninstall the game before deleting")
games_db.delete_game(self.id)
self.emit("game-removed")
force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=0.5)
¶
Forces termination of a running game, but only after a set time has elapsed; Invokes stop_game() when the game is dead.
Source code in lutris/game.py
def force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=.5):
"""Forces termination of a running game, but only after a set time has elapsed;
Invokes stop_game() when the game is dead."""
def death_watch():
"""Wait for the processes to die; returns True if do they all did."""
for _n in range(int(death_watch_seconds / death_watch_interval_seconds)):
time.sleep(death_watch_interval_seconds)
if not self.get_stop_pids():
return True
return False
def death_watch_cb(all_died, error):
"""Called after the death watch to more firmly kill any survivors."""
if error:
dialogs.ErrorDialog(str(error))
elif not all_died:
self.kill_processes(signal.SIGKILL)
# If we still can't kill everything, we'll still say we stopped it.
self.stop_game()
jobs.AsyncCall(death_watch, death_watch_cb)
force_stop(self)
¶
Source code in lutris/game.py
def force_stop(self):
# If force_stop_game fails, wait a few seconds and try SIGKILL on any survivors
self.runner.force_stop_game(self)
if self.get_stop_pids():
self.force_kill_delayed()
else:
self.stop_game()
get_browse_dir(self)
¶
Return the path to open with the Browse Files action.
Source code in lutris/game.py
def get_browse_dir(self):
"""Return the path to open with the Browse Files action."""
return self.runner.game_path
get_game_pids(self)
¶
Return a list of processes belonging to the Lutris game
Source code in lutris/game.py
def get_game_pids(self):
"""Return a list of processes belonging to the Lutris game"""
new_pids = self.get_new_pids()
game_pids = []
game_folder = self.runner.game_path or ""
for pid in new_pids:
cmdline = Process(pid).cmdline or ""
# pressure-vessel: This could potentially pick up PIDs not started by lutris?
if game_folder in cmdline or "pressure-vessel" in cmdline:
game_pids.append(pid)
return set(game_pids + [
pid for pid in new_pids
if Process(pid).environ.get("LUTRIS_GAME_UUID") == self.game_uuid
])
get_gameplay_info(self)
¶
Return the information provided by a runner's play method. Checks for possible errors.
Source code in lutris/game.py
def get_gameplay_info(self):
"""Return the information provided by a runner's play method.
Checks for possible errors.
"""
if not self.runner:
logger.warning("Trying to launch %s without a runner", self)
return {}
gameplay_info = self.runner.play()
if "error" in gameplay_info:
self.show_error_message(gameplay_info)
self.state = self.STATE_STOPPED
self.emit("game-stop")
return
if self.config.game_level.get("game", {}).get("launch_configs"):
configs = self.config.game_level["game"]["launch_configs"]
dlg = dialogs.LaunchConfigSelectDialog(self, configs)
if dlg.config_index:
config = configs[dlg.config_index - 1]
if "command" not in gameplay_info:
logger.debug("No command in %s", gameplay_info)
logger.debug(config)
return {}
gameplay_info["command"] = [gameplay_info["command"][0], config["exe"]]
if config.get("args"):
gameplay_info["command"] += strings.split_arguments(config["args"])
return gameplay_info
get_killswitch(self)
¶
Return the path to a file that is monitored during game execution. If the file stops existing, the game is stopped.
Source code in lutris/game.py
def get_killswitch(self):
"""Return the path to a file that is monitored during game execution.
If the file stops existing, the game is stopped.
"""
killswitch = self.runner.system_config.get("killswitch")
# Prevent setting a killswitch to a file that doesn't exists
if killswitch and system.path_exists(self.killswitch):
return killswitch
get_new_pids(self)
¶
Return list of PIDs started since the game was launched
Source code in lutris/game.py
def get_new_pids(self):
"""Return list of PIDs started since the game was launched"""
return set(system.get_running_pid_list()) - set(self.prelaunch_pids)
get_stop_pids(self)
¶
Finds the PIDs of processes that need killin'!
Source code in lutris/game.py
def get_stop_pids(self):
"""Finds the PIDs of processes that need killin'!"""
pids = self.get_game_pids()
if self.game_thread and self.game_thread.game_process:
pids.add(self.game_thread.game_process.pid)
return pids
get_terminal(self)
¶
Return the terminal used to run the game into or None if the game is not run from a terminal. Remember that only games using text mode should use the terminal.
Source code in lutris/game.py
def get_terminal(self):
"""Return the terminal used to run the game into or None if the game is not run from a terminal.
Remember that only games using text mode should use the terminal.
"""
if self.runner.system_config.get("terminal"):
terminal = self.runner.system_config.get("terminal_app", linux.get_default_terminal())
if terminal and not system.find_executable(terminal):
raise GameConfigError(_("The selected terminal application could not be launched:\n%s") % terminal)
return terminal
is_launchable(self)
¶
Verify that the current game can be launched.
Source code in lutris/game.py
def is_launchable(self):
"""Verify that the current game can be launched."""
if not self.is_installed:
logger.error("%s (%s) not installed", self, self.id)
dialogs.ErrorDialog(_("Tried to launch a game that isn't installed."))
return False
if not self.runner:
dialogs.ErrorDialog(_("Invalid game configuration: Missing runner"))
return False
if not self.runner.is_installed():
installed = self.runner.install_dialog()
if not installed:
dialogs.ErrorDialog(_("Runner not installed."))
return False
if self.runner.use_runtime():
runtime_updater = runtime.RuntimeUpdater()
if runtime_updater.is_updating():
dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected"))
if ("wine" in self.runner_name and not wine.get_wine_version() and not LINUX_SYSTEM.is_flatpak):
dialogs.WineNotInstalledWarning(parent=None)
return True
kill_processes(self, sig)
¶
Sends a signal to a process list, logging errors.
Source code in lutris/game.py
def kill_processes(self, sig):
"""Sends a signal to a process list, logging errors."""
pids = self.get_stop_pids()
for pid in pids:
try:
os.kill(int(pid), sig)
except ProcessLookupError as ex:
logger.debug("Failed to kill game process: %s", ex)
launch(self)
¶
Request launching a game. The game may not be installed yet.
Source code in lutris/game.py
def launch(self):
"""Request launching a game. The game may not be installed yet."""
if not self.is_launchable():
logger.error("Game is not launchable")
return
self.load_config() # Reload the config before launching it.
if str(self.id) in LOG_BUFFERS: # Reset game logs on each launch
log_buffer = LOG_BUFFERS[str(self.id)]
log_buffer.delete(log_buffer.get_start_iter(), log_buffer.get_end_iter())
self.state = self.STATE_LAUNCHING
self.prelaunch_pids = system.get_running_pid_list()
self.emit("game-start")
jobs.AsyncCall(self.runner.prelaunch, self.configure_game)
load_config(self)
¶
Load the game's configuration.
Source code in lutris/game.py
def load_config(self):
"""Load the game's configuration."""
if not self.is_installed:
return
self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id)
self.runner = self._get_runner()
move(self, new_location)
¶
Source code in lutris/game.py
def move(self, new_location):
logger.info("Moving %s to %s", self, new_location)
new_config = ""
old_location = self.directory
if os.path.exists(old_location):
game_directory = os.path.basename(old_location)
target_directory = os.path.join(new_location, game_directory)
else:
target_directory = new_location
self.directory = target_directory
self.save()
if not old_location:
logger.info("Previous location wasn't set. Cannot continue moving")
return target_directory
with open(self.config.game_config_path, encoding='utf-8') as config_file:
for line in config_file.readlines():
if target_directory in line:
new_config += line
else:
new_config += line.replace(old_location, target_directory)
with open(self.config.game_config_path, "w", encoding='utf-8') as config_file:
config_file.write(new_config)
if not system.path_exists(old_location):
logger.warning("Location %s doesn't exist, files already moved?", old_location)
return target_directory
if new_location.startswith(old_location):
logger.warning("Can't move %s to one of its children %s", old_location, new_location)
return target_directory
try:
shutil.move(old_location, new_location)
except OSError as ex:
logger.error(
"Failed to move %s to %s, you may have to move files manually (Exception: %s)",
old_location, new_location, ex
)
return target_directory
on_game_quit(self)
¶
Restore some settings and cleanup after game quit.
Source code in lutris/game.py
def on_game_quit(self):
"""Restore some settings and cleanup after game quit."""
if self.prelaunch_executor and self.prelaunch_executor.is_running:
logger.info("Stopping prelaunch script")
self.prelaunch_executor.stop()
self.heartbeat = None
if self.state != self.STATE_STOPPED:
logger.warning("Game still running (state: %s)", self.state)
self.stop()
# Check for post game script
postexit_command = self.runner.system_config.get("postexit_command")
if postexit_command:
command_array = shlex.split(postexit_command)
if system.path_exists(command_array[0]):
logger.info("Running post-exit command: %s", postexit_command)
postexit_thread = MonitoredCommand(
command_array,
include_processes=[os.path.basename(postexit_command)],
env=self.game_runtime_config["env"],
cwd=self.directory,
)
postexit_thread.start()
quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
logger.debug("%s stopped at %s", self.name, quit_time)
self.lastplayed = int(time.time())
self.save(save_config=False)
os.chdir(os.path.expanduser("~"))
if self.antimicro_thread:
self.antimicro_thread.stop()
if self.resolution_changed or self.runner.system_config.get("reset_desktop"):
DISPLAY_MANAGER.set_resolution(self.original_outputs)
if self.compositor_disabled:
self.set_desktop_compositing(True)
if self.screen_saver_inhibitor_cookie is not None:
SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie)
self.screen_saver_inhibitor_cookie = None
if self.runner.system_config.get("use_us_layout"):
with subprocess.Popen(["setxkbmap"], env=os.environ) as setxkbmap:
setxkbmap.communicate()
if self.runner.system_config.get("restore_gamma"):
restore_gamma()
self.process_return_codes()
prelaunch_beat(self)
¶
Watch the prelaunch command
Source code in lutris/game.py
def prelaunch_beat(self):
"""Watch the prelaunch command"""
if self.prelaunch_executor and self.prelaunch_executor.is_running:
return True
self.start_game()
return False
process_return_codes(self)
¶
Do things depending on how the game quitted.
Source code in lutris/game.py
def process_return_codes(self):
"""Do things depending on how the game quitted."""
if self.game_thread.return_code == 127:
# Error missing shared lib
error = "error while loading shared lib"
error_line = strings.lookup_string_in_text(error, self.game_thread.stdout)
if error_line:
dialogs.ErrorDialog(_("<b>Error: Missing shared library.</b>\n\n%s") % error_line)
if self.game_thread.return_code == 1:
# Error Wine version conflict
error = "maybe the wrong wineserver"
if strings.lookup_string_in_text(error, self.game_thread.stdout):
dialogs.ErrorDialog(_("<b>Error: A different Wine version is already using the same Wine prefix.</b>"))
remove(self, delete_files=False, no_signal=False)
¶
Uninstall a game
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
delete_files |
bool |
Delete the game files |
False |
no_signal |
bool |
Don't emit game-removed signal (if running in a thread) |
False |
Source code in lutris/game.py
def remove(self, delete_files=False, no_signal=False):
"""Uninstall a game
Params:
delete_files (bool): Delete the game files
no_signal (bool): Don't emit game-removed signal (if running in a thread)
"""
sql.db_update(settings.PGA_DB, "games", {"installed": 0, "runner": ""}, {"id": self.id})
if self.config:
self.config.remove()
xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True)
if delete_files and self.runner:
self.runner.remove_game_data(game_path=self.directory)
self.is_installed = False
self.runner = None
if no_signal:
return
self.emit("game-removed")
remove_from_favorites(self)
¶
Remove game from favorites
Source code in lutris/game.py
def remove_from_favorites(self):
"""Remove game from favorites"""
favorite = categories_db.get_category("favorite")
categories_db.remove_category_from_game(self.id, favorite["id"])
self.emit("game-updated")
restrict_to_display(self, display)
¶
Source code in lutris/game.py
def restrict_to_display(self, display):
outputs = DISPLAY_MANAGER.get_config()
if display == "primary":
display = None
for output in outputs:
if output.primary:
display = output.name
break
if not display:
logger.warning("No primary display set")
else:
found = False
for output in outputs:
if output.name == display:
found = True
break
if not found:
logger.warning("Selected display %s not found", display)
display = None
if display:
turn_off_except(display)
time.sleep(3)
return True
return False
save(self, save_config=False)
¶
Save the game's config and metadata, if save_config is set to False,
do not save the config. This is useful when exiting the game since the
config might have changed and we don't want to override the changes.
Source code in lutris/game.py
def save(self, save_config=False):
"""
Save the game's config and metadata, if `save_config` is set to False,
do not save the config. This is useful when exiting the game since the
config might have changed and we don't want to override the changes.
"""
if self.config:
logger.debug("Saving %s with config ID %s", self, self.config.game_config_id)
configpath = self.config.game_config_id
if save_config:
self.config.save()
else:
logger.warning("Saving %s without a configuration", self)
configpath = ""
self.set_platform_from_runner()
self.id = games_db.add_or_update(
name=self.name,
runner=self.runner_name,
slug=self.slug,
platform=self.platform,
directory=self.directory,
installed=self.is_installed,
year=self.year,
lastplayed=self.lastplayed,
configpath=configpath,
id=self.id,
playtime=self.playtime,
hidden=self.is_hidden,
service=self.service,
service_id=self.appid,
)
self.emit("game-updated")
set_desktop_compositing(self, enable)
¶
Enables or disables compositing
Source code in lutris/game.py
def set_desktop_compositing(self, enable):
"""Enables or disables compositing"""
if enable:
if self.compositor_disabled:
enable_compositing()
self.compositor_disabled = False
else:
if not self.compositor_disabled:
disable_compositing()
self.compositor_disabled = True
set_hidden(self, is_hidden)
¶
Do not show this game in the UI
Source code in lutris/game.py
def set_hidden(self, is_hidden):
"""Do not show this game in the UI"""
self.is_hidden = is_hidden
self.save()
self.emit("game-updated")
set_keyboard_layout(layout)
staticmethod
¶
Source code in lutris/game.py
@staticmethod
def set_keyboard_layout(layout):
setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"]
xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")]
with subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) as xkbcomp:
with subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin) as setxkbmap:
setxkbmap.communicate()
xkbcomp.communicate()
set_platform_from_runner(self)
¶
Set the game's platform from the runner
Source code in lutris/game.py
def set_platform_from_runner(self):
"""Set the game's platform from the runner"""
if not self.runner:
logger.warning("Game has no runner, can't set platform")
return
self.platform = self.runner.get_platform()
if not self.platform:
logger.warning("The %s runner didn't provide a platform for %s", self.runner.human_name, self)
show_error_message(message)
staticmethod
¶
Display an error message based on the runner's output.
Source code in lutris/game.py
@staticmethod
def show_error_message(message):
"""Display an error message based on the runner's output."""
if message["error"] == "CUSTOM":
message_text = message["text"].replace("&", "&")
dialogs.ErrorDialog(message_text)
elif message["error"] == "RUNNER_NOT_INSTALLED":
dialogs.ErrorDialog(_("Error the runner is not installed"))
elif message["error"] == "NO_BIOS":
dialogs.ErrorDialog(_("A bios file is required to run this game"))
elif message["error"] == "FILE_NOT_FOUND":
filename = message["file"]
if filename:
message_text = _("The file {} could not be found").format(filename.replace("&", "&"))
else:
message_text = _("This game has no executable set. The install process didn't finish properly.")
dialogs.ErrorDialog(message_text)
elif message["error"] == "NOT_EXECUTABLE":
message_text = message["file"].replace("&", "&")
dialogs.ErrorDialog(_("The file %s is not executable") % message_text)
elif message["error"] == "PATH_NOT_SET":
message_text = _("The path '%s' is not set. please set it in the options.") % message["path"]
dialogs.ErrorDialog(message_text)
else:
dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"])
start_antimicrox(self, antimicro_config)
¶
Start Antimicrox with a given config path
Source code in lutris/game.py
def start_antimicrox(self, antimicro_config):
"""Start Antimicrox with a given config path"""
antimicro_path = system.find_executable("antimicrox")
if not antimicro_path:
logger.warning("Antimicrox is not installed.")
return
logger.info("Starting Antic")
antimicro_command = [antimicro_path, "--hidden", "--tray", "--profile", antimicro_config]
self.antimicro_thread = MonitoredCommand(antimicro_command)
self.antimicro_thread.start()
start_game(self)
¶
Run a background command to lauch the game
Source code in lutris/game.py
def start_game(self):
"""Run a background command to lauch the game"""
self.game_thread = MonitoredCommand(
self.game_runtime_config["args"],
title=self.name,
runner=self.runner,
env=self.game_runtime_config["env"],
term=self.game_runtime_config["terminal"],
log_buffer=self.log_buffer,
include_processes=self.game_runtime_config["include_processes"],
exclude_processes=self.game_runtime_config["exclude_processes"],
)
if hasattr(self.runner, "stop"):
self.game_thread.stop_func = self.runner.stop
self.game_uuid = self.game_thread.env["LUTRIS_GAME_UUID"]
self.game_thread.start()
self.timer.start()
self.state = self.STATE_RUNNING
self.emit("game-started")
self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat)
with open(self.now_playing_path, "w", encoding="utf-8") as np_file:
np_file.write(self.name)
start_prelaunch_command(self, wait_for_completion=False)
¶
Start the prelaunch command specified in the system options
Source code in lutris/game.py
def start_prelaunch_command(self, wait_for_completion=False):
"""Start the prelaunch command specified in the system options"""
prelaunch_command = self.runner.system_config.get("prelaunch_command")
command_array = shlex.split(prelaunch_command)
if not system.path_exists(command_array[0]):
logger.warning("Command %s not found", command_array[0])
return
env = self.game_runtime_config["env"]
if wait_for_completion:
logger.info("Prelauch command: %s, waiting for completion", prelaunch_command)
# Monitor the prelaunch command and wait until it has finished
system.execute(command_array, env=env, cwd=self.directory)
else:
logger.info("Prelaunch command %s launched in the background", prelaunch_command)
self.prelaunch_executor = MonitoredCommand(
command_array,
include_processes=[os.path.basename(command_array[0])],
env=env,
cwd=self.directory,
)
self.prelaunch_executor.start()
start_xephyr(self, display=':2')
¶
Start a monitored Xephyr instance
Source code in lutris/game.py
def start_xephyr(self, display=":2"):
"""Start a monitored Xephyr instance"""
if not system.find_executable("Xephyr"):
raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option")
xephyr_command = get_xephyr_command(display, self.runner.system_config)
xephyr_thread = MonitoredCommand(xephyr_command)
xephyr_thread.start()
time.sleep(3)
return display
stop(self)
¶
Stops the game
Source code in lutris/game.py
def stop(self):
"""Stops the game"""
if self.state == self.STATE_STOPPED:
logger.debug("Game already stopped")
return
logger.info("Stopping %s", self)
if self.game_thread:
jobs.AsyncCall(self.game_thread.stop, None)
self.stop_game()
stop_game(self)
¶
Cleanup after a game as stopped
Source code in lutris/game.py
def stop_game(self):
"""Cleanup after a game as stopped"""
duration = self.timer.duration
logger.debug("%s has run for %s seconds", self, duration)
if duration < 5:
logger.warning("The game has run for a very short time, did it crash?")
# Inspect why it could have crashed
self.state = self.STATE_STOPPED
self.emit("game-stop")
if os.path.exists(self.now_playing_path):
os.unlink(self.now_playing_path)
if not self.timer.finished:
self.timer.end()
self.playtime += self.timer.duration / 3600
write_script(self, script_path)
¶
Output the launch argument in a bash script
Source code in lutris/game.py
def write_script(self, script_path):
"""Output the launch argument in a bash script"""
gameplay_info = self.get_gameplay_info()
if not gameplay_info:
logger.error("Unable to retrieve game information for %s. Can't write a script", self)
return
export_bash_script(self.runner, gameplay_info, script_path)
export_game(slug, dest_dir)
¶
Export a full game folder along with some lutris metadata
Source code in lutris/game.py
def export_game(slug, dest_dir):
"""Export a full game folder along with some lutris metadata"""
# List of runner where we know for sure that 1 folder = 1 game.
# For runners that handle ROMs, we have to handle this more finely.
# There is likely more than one game in a ROM folder but a ROM
# might have several files (like a bin/cue, or a multi-disk game)
exportable_runners = [
"linux",
"wine",
"dosbox",
"scummvm",
]
db_game = games_db.get_game_by_field(slug, "slug")
if db_game["runner"] not in exportable_runners:
raise RuntimeError("Game %s can't be exported." % db_game["name"])
if not db_game["directory"]:
raise RuntimeError("No game directory set. Could we guess it?")
game = Game(db_game["id"])
db_game["config"] = game.config.game_level
game_path = db_game["directory"]
config_path = os.path.join(db_game["directory"], "%s.lutris" % slug)
with open(config_path, "w", encoding="utf-8") as config_file:
json.dump(db_game, config_file, indent=2)
archive_path = os.path.join(dest_dir, "%s.7z" % slug)
_7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7z")
command = [_7zip_path, "a", archive_path, game_path]
return_code = subprocess.call(command)
if return_code != 0:
print("Creating of archive in %s failed with return code %s" % (archive_path, return_code))
import_game(file_path, dest_dir)
¶
Import a game in Lutris
Source code in lutris/game.py
def import_game(file_path, dest_dir):
"""Import a game in Lutris"""
if not os.path.exists(file_path):
raise RuntimeError("No file %s" % file_path)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
original_file_list = set(os.listdir(dest_dir))
extract.extract_7zip(file_path, dest_dir)
new_file_list = set(os.listdir(dest_dir))
new_dir = list(new_file_list - original_file_list)[0]
game_dir = os.path.join(dest_dir, new_dir)
game_config = [f for f in os.listdir(game_dir) if f.endswith(".lutris")][0]
with open(os.path.join(game_dir, game_config)) as config_file:
lutris_config = json.load(config_file)
# old_dir = lutris_config["directory"]
config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % lutris_config["configpath"])
write_yaml_to_file(lutris_config["config"], config_filename)
game_id = games_db.add_or_update(
name=lutris_config["name"],
runner=lutris_config["runner"],
slug=lutris_config["slug"],
platform=lutris_config["platform"],
directory=game_dir,
installed=lutris_config["installed"],
year=lutris_config["year"],
lastplayed=lutris_config["lastplayed"],
configpath=lutris_config["configpath"],
playtime=lutris_config["playtime"],
hidden=lutris_config["hidden"],
service=lutris_config["service"],
service_id=lutris_config["service_id"],
)
print("Added game with ID %s" % game_id)
game_actions
¶
Handle game specific actions
GameActions
¶
Regroup a list of callbacks for a game
Source code in lutris/game_actions.py
class GameActions:
"""Regroup a list of callbacks for a game"""
def __init__(self, application=None, window=None):
self.application = application or Gio.Application.get_default()
self.window = window
self.game_id = None
self._game = None
@property
def game(self):
if not self._game:
self._game = self.application.get_game_by_id(self.game_id)
if not self._game:
self._game = Game(self.game_id)
self._game.connect("game-error", self.window.on_game_error)
return self._game
@property
def is_game_running(self):
return bool(self.application.get_game_by_id(self.game_id))
def set_game(self, game=None, game_id=None):
if game:
self._game = game
self.game_id = game.id
else:
self._game = None
self.game_id = game_id
def get_game_actions(self):
"""Return a list of game actions and their callbacks"""
return [
("play", _("Play"), self.on_game_launch),
("stop", _("Stop"), self.on_game_stop),
("install", _("Install"), self.on_install_clicked),
("update", _("Install updates"), self.on_update_clicked),
("install_dlcs", "Install DLCs", self.on_install_dlc_clicked),
("show_logs", _("Show logs"), self.on_show_logs),
("add", _("Add installed game"), self.on_add_manually),
("duplicate", _("Duplicate"), self.on_game_duplicate),
("configure", _("Configure"), self.on_edit_game_configuration),
("favorite", _("Add to favorites"), self.on_add_favorite_game),
("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game),
("execute-script", _("Execute script"), self.on_execute_script_clicked),
("browse", _("Browse files"), self.on_browse_files),
(
"desktop-shortcut",
_("Create desktop shortcut"),
self.on_create_desktop_shortcut,
),
(
"rm-desktop-shortcut",
_("Delete desktop shortcut"),
self.on_remove_desktop_shortcut,
),
(
"menu-shortcut",
_("Create application menu shortcut"),
self.on_create_menu_shortcut,
),
(
"rm-menu-shortcut",
_("Delete application menu shortcut"),
self.on_remove_menu_shortcut,
),
("install_more", _("Install another version"), self.on_install_clicked),
("remove", _("Remove"), self.on_remove_game),
("view", _("View on Lutris.net"), self.on_view_game),
("hide", _("Hide game from library"), self.on_hide_game),
("unhide", _("Unhide game from library"), self.on_unhide_game),
]
def get_displayed_entries(self):
"""Return a dictionary of actions that should be shown for a game"""
return {
"add": not self.game.is_installed,
"duplicate": True,
"install": not self.game.is_installed,
"play": self.game.is_installed and not self.is_game_running,
"update": self.game.is_updatable,
"install_dlcs": self.game.is_updatable,
"stop": self.is_game_running,
"configure": bool(self.game.is_installed),
"browse": self.game.is_installed and self.game.runner_name != "browser",
"show_logs": self.game.is_installed,
"favorite": not self.game.is_favorite,
"deletefavorite": self.game.is_favorite,
"install_more": not self.game.service and self.game.is_installed,
"execute-script": bool(
self.game.is_installed and self.game.runner
and self.game.runner.system_config.get("manual_command")
),
"desktop-shortcut": (
self.game.is_installed
and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"menu-shortcut": (
self.game.is_installed
and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"rm-desktop-shortcut": bool(
self.game.is_installed
and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"rm-menu-shortcut": bool(
self.game.is_installed
and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"remove": True,
"view": True,
"hide": self.game.is_installed and not self.game.is_hidden,
"unhide": self.game.is_hidden,
}
def on_game_launch(self, *_args):
"""Launch a game"""
self.game.launch()
def get_running_game(self):
ids = self.application.get_running_game_ids()
for game_id in ids:
if str(game_id) == str(self.game.id):
return self.game
logger.warning("Game %s not in %s", self.game_id, ids)
def on_game_stop(self, _caller):
"""Stops the game"""
game = self.get_running_game()
if game:
game.force_stop()
def on_show_logs(self, _widget):
"""Display game log"""
_buffer = self.game.log_buffer
if not _buffer:
logger.info("No log for game %s", self.game)
return LogWindow(
title=_("Log for {}").format(self.game),
buffer=_buffer,
application=self.application
)
def on_install_clicked(self, *_args):
"""Install a game"""
# Install the currently selected game in the UI
if not self.game.slug:
raise RuntimeError("No game to install: %s" % self.game.id)
self.game.emit("game-install")
def on_update_clicked(self, _widget):
self.game.emit("game-install-update")
def on_install_dlc_clicked(self, _widget):
self.game.emit("game-install-dlc")
def on_locate_installed_game(self, _button, game):
"""Show the user a dialog to import an existing install to a DRM free service
Params:
game (Game): Game instance without a database ID, populated with a fields the service can provides
"""
AddGameDialog(self.window, game=game)
def on_add_manually(self, _widget, *_args):
"""Callback that presents the Add game dialog"""
return AddGameDialog(self.window, game=self.game, runner=self.game.runner_name)
def on_game_duplicate(self, _widget):
confirm_dlg = QuestionDialog(
{
"parent": self.window,
"question": _(
"Do you wish to duplicate %s?\nThe configuration will be duplicated, "
"but the games files will <b>not be duplicated</b>."
) % gtk_safe(self.game.name),
"title": _("Duplicate game?"),
}
)
if confirm_dlg.result != Gtk.ResponseType.YES:
return
assigned_name = get_unusued_game_name(self.game.name)
old_config_id = self.game.game_config_id
if old_config_id:
new_config_id = duplicate_game_config(self.game.slug, old_config_id)
else:
new_config_id = None
db_game = get_game_by_field(self.game.id, "id")
db_game["name"] = assigned_name
db_game["configpath"] = new_config_id
db_game.pop("id")
# Disconnect duplicate from service- there should be at most
# 1 PGA game for a service game.
db_game.pop("service", None)
db_game.pop("service_id", None)
game_id = add_game(**db_game)
new_game = Game(game_id)
new_game.save()
def on_edit_game_configuration(self, _widget):
"""Edit game preferences"""
self.application.show_window(EditGameConfigDialog, game=self.game, parent=self.window)
def on_add_favorite_game(self, _widget):
"""Add to favorite Games list"""
self.game.add_to_favorites()
def on_delete_favorite_game(self, _widget):
"""delete from favorites"""
self.game.remove_from_favorites()
def on_hide_game(self, _widget):
"""Add a game to the list of hidden games"""
self.game.set_hidden(True)
def on_unhide_game(self, _widget):
"""Removes a game from the list of hidden games"""
self.game.set_hidden(False)
def on_execute_script_clicked(self, _widget):
"""Execute the game's associated script"""
manual_command = self.game.runner.system_config.get("manual_command")
if path_exists(manual_command):
MonitoredCommand(
[manual_command],
include_processes=[os.path.basename(manual_command)],
cwd=self.game.directory,
).start()
logger.info("Running %s in the background", manual_command)
def on_browse_files(self, _widget):
"""Callback to open a game folder in the file browser"""
path = self.game.get_browse_dir()
if not path:
dialogs.NoticeDialog(_("This game has no installation directory"))
elif path_exists(path):
open_uri("file://%s" % path)
else:
dialogs.NoticeDialog(_("Can't open %s \nThe folder doesn't exist.") % path)
def on_create_menu_shortcut(self, *_args):
"""Add the selected game to the system's Games menu."""
xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True)
def on_create_desktop_shortcut(self, *_args):
"""Create a desktop launcher for the selected game."""
xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True)
def on_remove_menu_shortcut(self, *_args):
"""Remove an XDG menu shortcut"""
xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)
def on_remove_desktop_shortcut(self, *_args):
"""Remove a .desktop shortcut"""
xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True)
def on_view_game(self, _widget):
"""Callback to open a game on lutris.net"""
open_uri("https://lutris.net/games/%s" % self.game.slug)
def on_remove_game(self, *_args):
"""Callback that present the uninstall dialog to the user"""
if self.game.is_installed:
UninstallGameDialog(game_id=self.game.id, parent=self.window)
else:
RemoveGameDialog(game_id=self.game.id, parent=self.window)
game
property
readonly
¶
is_game_running
property
readonly
¶
__init__(self, application=None, window=None)
special
¶
Source code in lutris/game_actions.py
def __init__(self, application=None, window=None):
self.application = application or Gio.Application.get_default()
self.window = window
self.game_id = None
self._game = None
get_displayed_entries(self)
¶
Return a dictionary of actions that should be shown for a game
Source code in lutris/game_actions.py
def get_displayed_entries(self):
"""Return a dictionary of actions that should be shown for a game"""
return {
"add": not self.game.is_installed,
"duplicate": True,
"install": not self.game.is_installed,
"play": self.game.is_installed and not self.is_game_running,
"update": self.game.is_updatable,
"install_dlcs": self.game.is_updatable,
"stop": self.is_game_running,
"configure": bool(self.game.is_installed),
"browse": self.game.is_installed and self.game.runner_name != "browser",
"show_logs": self.game.is_installed,
"favorite": not self.game.is_favorite,
"deletefavorite": self.game.is_favorite,
"install_more": not self.game.service and self.game.is_installed,
"execute-script": bool(
self.game.is_installed and self.game.runner
and self.game.runner.system_config.get("manual_command")
),
"desktop-shortcut": (
self.game.is_installed
and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"menu-shortcut": (
self.game.is_installed
and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"rm-desktop-shortcut": bool(
self.game.is_installed
and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"rm-menu-shortcut": bool(
self.game.is_installed
and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"remove": True,
"view": True,
"hide": self.game.is_installed and not self.game.is_hidden,
"unhide": self.game.is_hidden,
}
get_game_actions(self)
¶
Return a list of game actions and their callbacks
Source code in lutris/game_actions.py
def get_game_actions(self):
"""Return a list of game actions and their callbacks"""
return [
("play", _("Play"), self.on_game_launch),
("stop", _("Stop"), self.on_game_stop),
("install", _("Install"), self.on_install_clicked),
("update", _("Install updates"), self.on_update_clicked),
("install_dlcs", "Install DLCs", self.on_install_dlc_clicked),
("show_logs", _("Show logs"), self.on_show_logs),
("add", _("Add installed game"), self.on_add_manually),
("duplicate", _("Duplicate"), self.on_game_duplicate),
("configure", _("Configure"), self.on_edit_game_configuration),
("favorite", _("Add to favorites"), self.on_add_favorite_game),
("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game),
("execute-script", _("Execute script"), self.on_execute_script_clicked),
("browse", _("Browse files"), self.on_browse_files),
(
"desktop-shortcut",
_("Create desktop shortcut"),
self.on_create_desktop_shortcut,
),
(
"rm-desktop-shortcut",
_("Delete desktop shortcut"),
self.on_remove_desktop_shortcut,
),
(
"menu-shortcut",
_("Create application menu shortcut"),
self.on_create_menu_shortcut,
),
(
"rm-menu-shortcut",
_("Delete application menu shortcut"),
self.on_remove_menu_shortcut,
),
("install_more", _("Install another version"), self.on_install_clicked),
("remove", _("Remove"), self.on_remove_game),
("view", _("View on Lutris.net"), self.on_view_game),
("hide", _("Hide game from library"), self.on_hide_game),
("unhide", _("Unhide game from library"), self.on_unhide_game),
]
get_running_game(self)
¶
Source code in lutris/game_actions.py
def get_running_game(self):
ids = self.application.get_running_game_ids()
for game_id in ids:
if str(game_id) == str(self.game.id):
return self.game
logger.warning("Game %s not in %s", self.game_id, ids)
on_add_favorite_game(self, _widget)
¶
Add to favorite Games list
Source code in lutris/game_actions.py
def on_add_favorite_game(self, _widget):
"""Add to favorite Games list"""
self.game.add_to_favorites()
on_add_manually(self, _widget, *_args)
¶
Callback that presents the Add game dialog
Source code in lutris/game_actions.py
def on_add_manually(self, _widget, *_args):
"""Callback that presents the Add game dialog"""
return AddGameDialog(self.window, game=self.game, runner=self.game.runner_name)
on_browse_files(self, _widget)
¶
Callback to open a game folder in the file browser
Source code in lutris/game_actions.py
def on_browse_files(self, _widget):
"""Callback to open a game folder in the file browser"""
path = self.game.get_browse_dir()
if not path:
dialogs.NoticeDialog(_("This game has no installation directory"))
elif path_exists(path):
open_uri("file://%s" % path)
else:
dialogs.NoticeDialog(_("Can't open %s \nThe folder doesn't exist.") % path)
on_create_desktop_shortcut(self, *_args)
¶
Create a desktop launcher for the selected game.
Source code in lutris/game_actions.py
def on_create_desktop_shortcut(self, *_args):
"""Create a desktop launcher for the selected game."""
xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True)
on_create_menu_shortcut(self, *_args)
¶
Add the selected game to the system's Games menu.
Source code in lutris/game_actions.py
def on_create_menu_shortcut(self, *_args):
"""Add the selected game to the system's Games menu."""
xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True)
on_delete_favorite_game(self, _widget)
¶
delete from favorites
Source code in lutris/game_actions.py
def on_delete_favorite_game(self, _widget):
"""delete from favorites"""
self.game.remove_from_favorites()
on_edit_game_configuration(self, _widget)
¶
Edit game preferences
Source code in lutris/game_actions.py
def on_edit_game_configuration(self, _widget):
"""Edit game preferences"""
self.application.show_window(EditGameConfigDialog, game=self.game, parent=self.window)
on_execute_script_clicked(self, _widget)
¶
Execute the game's associated script
Source code in lutris/game_actions.py
def on_execute_script_clicked(self, _widget):
"""Execute the game's associated script"""
manual_command = self.game.runner.system_config.get("manual_command")
if path_exists(manual_command):
MonitoredCommand(
[manual_command],
include_processes=[os.path.basename(manual_command)],
cwd=self.game.directory,
).start()
logger.info("Running %s in the background", manual_command)
on_game_duplicate(self, _widget)
¶
Source code in lutris/game_actions.py
def on_game_duplicate(self, _widget):
confirm_dlg = QuestionDialog(
{
"parent": self.window,
"question": _(
"Do you wish to duplicate %s?\nThe configuration will be duplicated, "
"but the games files will <b>not be duplicated</b>."
) % gtk_safe(self.game.name),
"title": _("Duplicate game?"),
}
)
if confirm_dlg.result != Gtk.ResponseType.YES:
return
assigned_name = get_unusued_game_name(self.game.name)
old_config_id = self.game.game_config_id
if old_config_id:
new_config_id = duplicate_game_config(self.game.slug, old_config_id)
else:
new_config_id = None
db_game = get_game_by_field(self.game.id, "id")
db_game["name"] = assigned_name
db_game["configpath"] = new_config_id
db_game.pop("id")
# Disconnect duplicate from service- there should be at most
# 1 PGA game for a service game.
db_game.pop("service", None)
db_game.pop("service_id", None)
game_id = add_game(**db_game)
new_game = Game(game_id)
new_game.save()
on_game_launch(self, *_args)
¶
Launch a game
Source code in lutris/game_actions.py
def on_game_launch(self, *_args):
"""Launch a game"""
self.game.launch()
on_game_stop(self, _caller)
¶
Stops the game
Source code in lutris/game_actions.py
def on_game_stop(self, _caller):
"""Stops the game"""
game = self.get_running_game()
if game:
game.force_stop()
on_hide_game(self, _widget)
¶
Add a game to the list of hidden games
Source code in lutris/game_actions.py
def on_hide_game(self, _widget):
"""Add a game to the list of hidden games"""
self.game.set_hidden(True)
on_install_clicked(self, *_args)
¶
Install a game
Source code in lutris/game_actions.py
def on_install_clicked(self, *_args):
"""Install a game"""
# Install the currently selected game in the UI
if not self.game.slug:
raise RuntimeError("No game to install: %s" % self.game.id)
self.game.emit("game-install")
on_install_dlc_clicked(self, _widget)
¶
Source code in lutris/game_actions.py
def on_install_dlc_clicked(self, _widget):
self.game.emit("game-install-dlc")
on_locate_installed_game(self, _button, game)
¶
Show the user a dialog to import an existing install to a DRM free service
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game |
Game |
Game instance without a database ID, populated with a fields the service can provides |
required |
Source code in lutris/game_actions.py
def on_locate_installed_game(self, _button, game):
"""Show the user a dialog to import an existing install to a DRM free service
Params:
game (Game): Game instance without a database ID, populated with a fields the service can provides
"""
AddGameDialog(self.window, game=game)
on_remove_desktop_shortcut(self, *_args)
¶
Remove a .desktop shortcut
Source code in lutris/game_actions.py
def on_remove_desktop_shortcut(self, *_args):
"""Remove a .desktop shortcut"""
xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True)
on_remove_game(self, *_args)
¶
Callback that present the uninstall dialog to the user
Source code in lutris/game_actions.py
def on_remove_game(self, *_args):
"""Callback that present the uninstall dialog to the user"""
if self.game.is_installed:
UninstallGameDialog(game_id=self.game.id, parent=self.window)
else:
RemoveGameDialog(game_id=self.game.id, parent=self.window)
on_remove_menu_shortcut(self, *_args)
¶
Remove an XDG menu shortcut
Source code in lutris/game_actions.py
def on_remove_menu_shortcut(self, *_args):
"""Remove an XDG menu shortcut"""
xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)
on_show_logs(self, _widget)
¶
Display game log
Source code in lutris/game_actions.py
def on_show_logs(self, _widget):
"""Display game log"""
_buffer = self.game.log_buffer
if not _buffer:
logger.info("No log for game %s", self.game)
return LogWindow(
title=_("Log for {}").format(self.game),
buffer=_buffer,
application=self.application
)
on_unhide_game(self, _widget)
¶
Removes a game from the list of hidden games
Source code in lutris/game_actions.py
def on_unhide_game(self, _widget):
"""Removes a game from the list of hidden games"""
self.game.set_hidden(False)
on_update_clicked(self, _widget)
¶
Source code in lutris/game_actions.py
def on_update_clicked(self, _widget):
self.game.emit("game-install-update")
on_view_game(self, _widget)
¶
Callback to open a game on lutris.net
Source code in lutris/game_actions.py
def on_view_game(self, _widget):
"""Callback to open a game on lutris.net"""
open_uri("https://lutris.net/games/%s" % self.game.slug)
set_game(self, game=None, game_id=None)
¶
Source code in lutris/game_actions.py
def set_game(self, game=None, game_id=None):
if game:
self._game = game
self.game_id = game.id
else:
self._game = None
self.game_id = game_id
gui
special
¶
Lutris GUI package
addgameswindow
¶
AddGamesWindow (BaseApplicationWindow)
¶
Show a selection of ways to add games to Lutris
Source code in lutris/gui/addgameswindow.py
class AddGamesWindow(BaseApplicationWindow): # pylint: disable=too-many-public-methods
"""Show a selection of ways to add games to Lutris"""
sections = [
(
"system-search-symbolic",
_("Search the Lutris website for installers"),
_("Query our website for community installers"),
"search_installers"
),
(
"folder-new-symbolic",
_("Scan a folder for games"),
_("Mass-import a folder of games"),
"scan_folder"
),
(
"media-optical-dvd-symbolic",
_("Install a Windows game from media"),
_("Launch a setup file from an optical drive or download"),
"install_from_setup"
),
(
"x-office-document-symbolic",
_("Install from a local install script"),
_("Run a YAML install script"),
"install_from_script"
),
(
"list-add-symbolic",
_("Add locally installed game"),
_("Manually configure a game available locally"),
"add_local_game"
)
]
title_text = _("Add games to Lutris")
def __init__(self, application=None):
super().__init__(application=application)
self.set_default_size(640, 450)
self.search_timer_id = None
self.search_spinner = None
self.text_query = None
self.result_label = None
self.title_label = Gtk.Label(visible=True)
self.title_label.set_markup(f"<b>{self.title_text}</b>")
self.vbox.pack_start(self.title_label, False, False, 12)
self.listbox = Gtk.ListBox(visible=True)
self.listbox.set_activate_on_single_click(True)
self.vbox.pack_start(self.listbox, False, False, 12)
for icon, text, subtext, callback_name in self.sections:
row = self.build_row(icon, text, subtext)
row.callback_name = callback_name
self.listbox.add(row)
self.listbox.connect("row-activated", self.on_row_activated)
def on_row_activated(self, listbox, row):
if row.callback_name:
callback = getattr(self, row.callback_name)
callback()
def _get_row(self):
row = Gtk.ListBoxRow(visible=True)
row.set_selectable(False)
row.set_activatable(True)
return row
def _get_box(self):
return Gtk.Box(
spacing=12,
margin_right=12,
margin_left=12,
margin_top=12,
margin_bottom=12,
visible=True,
)
def _get_icon(self, name, small=False):
if small:
size = Gtk.IconSize.MENU
else:
size = Gtk.IconSize.DND
icon = Gtk.Image.new_from_icon_name(name, size)
icon.show()
return icon
def _get_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup(text)
label.set_alignment(0, 0.5)
return label
def build_row(self, icon_name, text, subtext):
row = self._get_row()
box = self._get_box()
if icon_name:
icon = self._get_icon(icon_name)
box.pack_start(icon, False, False, 0)
label = self._get_label(f"<b>{text}</b>\n{subtext}")
box.pack_start(label, True, True, 0)
if icon_name:
next_icon = self._get_icon("go-next-symbolic", small=True)
box.pack_start(next_icon, False, False, 0)
row.add(box)
return row
def search_installers(self):
"""Search installers with the Lutris API"""
self.title_label.set_markup("<b>Search Lutris.net</b>")
self.listbox.destroy()
hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, visible=True)
entry = Gtk.SearchEntry(visible=True)
hbox.pack_start(entry, True, True, 0)
self.search_spinner = Gtk.Spinner(visible=False)
hbox.pack_end(self.search_spinner, False, False, 6)
self.vbox.add(hbox)
self.result_label = self._get_label("")
self.vbox.add(self.result_label)
entry.connect("changed", self._on_search_updated)
self.listbox = Gtk.ListBox()
self.listbox.connect("row-activated", self._on_game_selected)
scroll = Gtk.ScrolledWindow(visible=True)
scroll.set_vexpand(True)
scroll.add(self.listbox)
self.vbox.add(scroll)
entry.grab_focus()
def scan_folder(self):
"""Scan a folder of already installed games"""
self.title_label.set_markup("<b>Import games from a folder</b>")
self.listbox.destroy()
script_dlg = DirectoryDialog(_("Select folder to scan"))
if not script_dlg.folder:
self.destroy()
return
spinner = Gtk.Spinner(visible=True)
spinner.start()
self.vbox.pack_start(spinner, False, False, 18)
AsyncCall(scan_directory, self._on_folder_scanned, script_dlg.folder)
def _on_folder_scanned(self, result, error):
if error:
ErrorDialog(error)
self.destroy()
return
for child in self.vbox.get_children():
child.destroy()
installed, missing = result
installed_label = self._get_label("Installed games")
self.vbox.add(installed_label)
installed_listbox = Gtk.ListBox(visible=True)
installed_scroll = Gtk.ScrolledWindow(visible=True)
installed_scroll.set_vexpand(True)
installed_scroll.add(installed_listbox)
self.vbox.add(installed_scroll)
for folder in installed:
installed_listbox.add(self.build_row("", gtk_safe(folder), ""))
missing_label = self._get_label("No match found")
self.vbox.add(missing_label)
missing_listbox = Gtk.ListBox(visible=True)
missing_scroll = Gtk.ScrolledWindow(visible=True)
missing_scroll.set_vexpand(True)
missing_scroll.add(missing_listbox)
self.vbox.add(missing_scroll)
for folder in missing:
missing_listbox.add(self.build_row("", gtk_safe(folder), ""))
def _on_search_updated(self, entry):
if self.search_timer_id:
GLib.source_remove(self.search_timer_id)
self.text_query = entry.get_text().strip()
self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
def _on_game_selected(self, listbox, row):
game_slug = row.api_info["slug"]
installers = get_installers(game_slug=game_slug)
application = Gio.Application.get_default()
application.show_installer_window(installers)
self.destroy()
def update_search_results(self):
# Don't start a search while another is going; defer it instead.
if self.search_spinner.get_visible():
self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
return
self.search_timer_id = None
if self.text_query:
self.search_spinner.show()
self.search_spinner.start()
AsyncCall(api.search_games, self.update_search_results_cb, self.text_query)
def update_search_results_cb(self, api_games, error):
if error:
ErrorDialog(error)
return
self.search_spinner.stop()
self.search_spinner.hide()
total_count = api_games.get("count", 0)
count = len(api_games.get('results', []))
if not count:
self.result_label.set_markup(_("No results"))
elif count == total_count:
self.result_label.set_markup(_(f"Showing <b>{count}</b> results"))
else:
self.result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
for row in self.listbox.get_children():
row.destroy()
for game in api_games.get("results", []):
platforms = ",".join(gtk_safe(platform["name"]) for platform in game["platforms"])
year = game['year'] or ""
if platforms and year:
platforms = ", " + platforms
row = self.build_row("", gtk_safe(game['name']), f"{year}{platforms}")
row.api_info = game
self.listbox.add(row)
self.listbox.show()
def install_from_setup(self):
"""Install from a setup file"""
self.title_label.set_markup(_("<b>Select setup file</b>"))
self.listbox.destroy()
label = self._get_label("Game name")
self.vbox.add(label)
entry = Gtk.Entry(visible=True)
self.vbox.add(entry)
button = Gtk.Button(_("Continue"), visible=True)
button.connect("clicked", self._on_install_setup_continue, entry)
self.vbox.add(button)
def _on_install_setup_continue(self, button, entry):
name = entry.get_text().strip()
installer = {
"name": name,
"version": "Setup file",
"slug": slugify(name) + "-setup",
"game_slug": slugify(name),
"runner": "wine",
"script": {
"game": {
"exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"
},
"files": [
{"setupfile": "N/A:Select the setup file"}
],
"installer": [
{"task": {"name": "wineexec", "executable": "setupfile"}}
]
}
}
application = Gio.Application.get_default()
application.show_installer_window([installer])
self.destroy()
def install_from_script(self):
"""Install from a YAML file"""
script_dlg = FileDialog(_("Select a Lutris installer"))
if script_dlg.filename:
installers = get_installers(installer_file=script_dlg.filename)
application = Gio.Application.get_default()
application.show_installer_window(installers)
self.destroy()
def add_local_game(self):
"""Manually configure game"""
AddGameDialog(None)
self.destroy()
sections
¶
title_text
¶
__init__(self, application=None)
special
¶
Source code in lutris/gui/addgameswindow.py
def __init__(self, application=None):
super().__init__(application=application)
self.set_default_size(640, 450)
self.search_timer_id = None
self.search_spinner = None
self.text_query = None
self.result_label = None
self.title_label = Gtk.Label(visible=True)
self.title_label.set_markup(f"<b>{self.title_text}</b>")
self.vbox.pack_start(self.title_label, False, False, 12)
self.listbox = Gtk.ListBox(visible=True)
self.listbox.set_activate_on_single_click(True)
self.vbox.pack_start(self.listbox, False, False, 12)
for icon, text, subtext, callback_name in self.sections:
row = self.build_row(icon, text, subtext)
row.callback_name = callback_name
self.listbox.add(row)
self.listbox.connect("row-activated", self.on_row_activated)
add_local_game(self)
¶
Manually configure game
Source code in lutris/gui/addgameswindow.py
def add_local_game(self):
"""Manually configure game"""
AddGameDialog(None)
self.destroy()
build_row(self, icon_name, text, subtext)
¶
Source code in lutris/gui/addgameswindow.py
def build_row(self, icon_name, text, subtext):
row = self._get_row()
box = self._get_box()
if icon_name:
icon = self._get_icon(icon_name)
box.pack_start(icon, False, False, 0)
label = self._get_label(f"<b>{text}</b>\n{subtext}")
box.pack_start(label, True, True, 0)
if icon_name:
next_icon = self._get_icon("go-next-symbolic", small=True)
box.pack_start(next_icon, False, False, 0)
row.add(box)
return row
install_from_script(self)
¶
Install from a YAML file
Source code in lutris/gui/addgameswindow.py
def install_from_script(self):
"""Install from a YAML file"""
script_dlg = FileDialog(_("Select a Lutris installer"))
if script_dlg.filename:
installers = get_installers(installer_file=script_dlg.filename)
application = Gio.Application.get_default()
application.show_installer_window(installers)
self.destroy()
install_from_setup(self)
¶
Install from a setup file
Source code in lutris/gui/addgameswindow.py
def install_from_setup(self):
"""Install from a setup file"""
self.title_label.set_markup(_("<b>Select setup file</b>"))
self.listbox.destroy()
label = self._get_label("Game name")
self.vbox.add(label)
entry = Gtk.Entry(visible=True)
self.vbox.add(entry)
button = Gtk.Button(_("Continue"), visible=True)
button.connect("clicked", self._on_install_setup_continue, entry)
self.vbox.add(button)
on_row_activated(self, listbox, row)
¶
Source code in lutris/gui/addgameswindow.py
def on_row_activated(self, listbox, row):
if row.callback_name:
callback = getattr(self, row.callback_name)
callback()
scan_folder(self)
¶
Scan a folder of already installed games
Source code in lutris/gui/addgameswindow.py
def scan_folder(self):
"""Scan a folder of already installed games"""
self.title_label.set_markup("<b>Import games from a folder</b>")
self.listbox.destroy()
script_dlg = DirectoryDialog(_("Select folder to scan"))
if not script_dlg.folder:
self.destroy()
return
spinner = Gtk.Spinner(visible=True)
spinner.start()
self.vbox.pack_start(spinner, False, False, 18)
AsyncCall(scan_directory, self._on_folder_scanned, script_dlg.folder)
search_installers(self)
¶
Search installers with the Lutris API
Source code in lutris/gui/addgameswindow.py
def search_installers(self):
"""Search installers with the Lutris API"""
self.title_label.set_markup("<b>Search Lutris.net</b>")
self.listbox.destroy()
hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, visible=True)
entry = Gtk.SearchEntry(visible=True)
hbox.pack_start(entry, True, True, 0)
self.search_spinner = Gtk.Spinner(visible=False)
hbox.pack_end(self.search_spinner, False, False, 6)
self.vbox.add(hbox)
self.result_label = self._get_label("")
self.vbox.add(self.result_label)
entry.connect("changed", self._on_search_updated)
self.listbox = Gtk.ListBox()
self.listbox.connect("row-activated", self._on_game_selected)
scroll = Gtk.ScrolledWindow(visible=True)
scroll.set_vexpand(True)
scroll.add(self.listbox)
self.vbox.add(scroll)
entry.grab_focus()
update_search_results(self)
¶
Source code in lutris/gui/addgameswindow.py
def update_search_results(self):
# Don't start a search while another is going; defer it instead.
if self.search_spinner.get_visible():
self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
return
self.search_timer_id = None
if self.text_query:
self.search_spinner.show()
self.search_spinner.start()
AsyncCall(api.search_games, self.update_search_results_cb, self.text_query)
update_search_results_cb(self, api_games, error)
¶
Source code in lutris/gui/addgameswindow.py
def update_search_results_cb(self, api_games, error):
if error:
ErrorDialog(error)
return
self.search_spinner.stop()
self.search_spinner.hide()
total_count = api_games.get("count", 0)
count = len(api_games.get('results', []))
if not count:
self.result_label.set_markup(_("No results"))
elif count == total_count:
self.result_label.set_markup(_(f"Showing <b>{count}</b> results"))
else:
self.result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
for row in self.listbox.get_children():
row.destroy()
for game in api_games.get("results", []):
platforms = ",".join(gtk_safe(platform["name"]) for platform in game["platforms"])
year = game['year'] or ""
if platforms and year:
platforms = ", " + platforms
row = self.build_row("", gtk_safe(game['name']), f"{year}{platforms}")
row.api_info = game
self.listbox.add(row)
self.listbox.show()
application
¶
Application (Application)
¶
Source code in lutris/gui/application.py
class Application(Gtk.Application):
def __init__(self):
super().__init__(
application_id="net.lutris.Lutris",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
)
GObject.add_emission_hook(Game, "game-launch", self.on_game_launch)
GObject.add_emission_hook(Game, "game-start", self.on_game_start)
GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
GObject.add_emission_hook(Game, "game-install", self.on_game_install)
GObject.add_emission_hook(Game, "game-install-update", self.on_game_install_update)
GObject.add_emission_hook(Game, "game-install-dlc", self.on_game_install_dlc)
GLib.set_application_name(_("Lutris"))
self.window = None
self.running_games = Gio.ListStore.new(Game)
self.app_windows = {}
self.tray = None
self.css_provider = Gtk.CssProvider.new()
self.run_in_background = False
self.style_manager = None
if os.geteuid() == 0:
ErrorDialog(_("Running Lutris as root is not recommended and may cause unexpected issues"))
try:
self.css_provider.load_from_path(os.path.join(datapath.get(), "ui", "lutris.css"))
except GLib.Error as e:
logger.exception(e)
if hasattr(self, "add_main_option"):
self.add_arguments()
else:
ErrorDialog(_("Your Linux distribution is too old. Lutris won't function properly."))
def add_arguments(self):
if hasattr(self, "set_option_context_summary"):
self.set_option_context_summary(_(
"Run a game directly by adding the parameter lutris:rungame/game-identifier.\n"
"If several games share the same identifier you can use the numerical ID "
"(displayed when running lutris --list-games) and add "
"lutris:rungameid/numerical-id.\n"
"To install a game, add lutris:install/game-identifier."
))
else:
logger.warning("GLib.set_option_context_summary missing, " "was added in GLib 2.56 (Released 2018-03-12)")
self.add_main_option(
"version",
ord("v"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Print the version of Lutris and exit"),
None,
)
self.add_main_option(
"debug",
ord("d"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Show debug messages"),
None,
)
self.add_main_option(
"install",
ord("i"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Install a game from a yml file"),
None,
)
self.add_main_option(
"output-script",
ord("b"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Generate a bash script to run a game without the client"),
None,
)
self.add_main_option(
"exec",
ord("e"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Execute a program with the Lutris Runtime"),
None,
)
self.add_main_option(
"list-games",
ord("l"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all games in database"),
None,
)
self.add_main_option(
"installed",
ord("o"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Only list installed games"),
None,
)
self.add_main_option(
"list-steam-games",
ord("s"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List available Steam games"),
None,
)
self.add_main_option(
"list-steam-folders",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known Steam library folders"),
None,
)
self.add_main_option(
"list-runners",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known runners"),
None,
)
self.add_main_option(
"list-wine-versions",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known Wine versions"),
None,
)
self.add_main_option(
"install-runner",
ord("r"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Install a Runner"),
None,
)
self.add_main_option(
"uninstall-runner",
ord("u"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Uninstall a Runner"),
None,
)
self.add_main_option(
"export",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Export a game"),
None,
)
self.add_main_option(
"import",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Import a game"),
None,
)
self.add_main_option(
"dest",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Destination path for export"),
None,
)
self.add_main_option(
"json",
ord("j"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Display the list of games in JSON format"),
None,
)
self.add_main_option(
"reinstall",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Reinstall game"),
None,
)
self.add_main_option("submit-issue", 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Submit an issue"), None)
self.add_main_option(
GLib.OPTION_REMAINING,
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING_ARRAY,
_("URI to open"),
"URI",
)
def do_startup(self): # pylint: disable=arguments-differ
"""Sets up the application on first start."""
Gtk.Application.do_startup(self)
signal.signal(signal.SIGINT, signal.SIG_DFL)
action = Gio.SimpleAction.new("quit")
action.connect("activate", lambda *x: self.quit())
self.add_action(action)
self.add_accelerator("<Primary>q", "app.quit")
self.style_manager = StyleManager()
def do_activate(self): # pylint: disable=arguments-differ
Application.show_update_runtime_dialog()
if not self.window:
self.window = LutrisWindow(application=self)
screen = self.window.props.screen # pylint: disable=no-member
Gtk.StyleContext.add_provider_for_screen(screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
if not self.run_in_background:
self.window.present()
else:
# Reset run in background to False. Future calls will set it
# accordingly
self.run_in_background = False
@staticmethod
def show_update_runtime_dialog():
if os.environ.get("LUTRIS_SKIP_INIT"):
logger.debug("Skipping initialization")
else:
init_dialog = LutrisInitDialog(update_runtime)
init_dialog.run()
def get_window_key(self, **kwargs):
if kwargs.get("appid"):
return kwargs["appid"]
if kwargs.get("runner"):
return kwargs["runner"].name
if kwargs.get("installers"):
return kwargs["installers"][0]["game_slug"]
if kwargs.get("game"):
return str(kwargs["game"].id)
return str(kwargs)
def show_window(self, window_class, **kwargs):
"""Instanciate a window keeping 1 instance max
Params:
window_class (Gtk.Window): class to create the instance from
kwargs (dict): Additional arguments to pass to the instanciated window
Returns:
Gtk.Window: the existing window instance or a newly created one
"""
window_key = str(window_class.__name__) + self.get_window_key(**kwargs)
if self.app_windows.get(window_key):
self.app_windows[window_key].present()
return self.app_windows[window_key]
if issubclass(window_class, Gtk.Dialog):
if "parent" in kwargs:
window_inst = window_class(**kwargs)
else:
window_inst = window_class(parent=self.window, **kwargs)
window_inst.set_application(self)
else:
window_inst = window_class(application=self, **kwargs)
window_inst.connect("destroy", self.on_app_window_destroyed, self.get_window_key(**kwargs))
self.app_windows[window_key] = window_inst
logger.debug("Showing window %s", window_key)
window_inst.show()
return window_inst
def show_installer_window(self, installers, service=None, appid=None, is_update=False):
self.show_window(
InstallerWindow,
installers=installers,
service=service,
appid=appid,
is_update=is_update
)
def on_app_window_destroyed(self, app_window, window_key):
"""Remove the reference to the window when it has been destroyed"""
window_key = str(app_window.__class__.__name__) + window_key
try:
del self.app_windows[window_key]
logger.debug("Removed window %s", window_key)
except KeyError:
logger.warning("Failed to remove window %s", window_key)
logger.info("Available windows: %s", ", ".join(self.app_windows.keys()))
return True
@staticmethod
def _print(command_line, string):
# Workaround broken pygobject bindings
command_line.do_print_literal(command_line, string + "\n")
def generate_script(self, db_game, script_path):
"""Output a script to a file.
The script is capable of launching a game without the client
"""
game = Game(db_game["id"])
game.load_config()
game.write_script(script_path)
def do_command_line(self, command_line): # noqa: C901 # pylint: disable=arguments-differ
# pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
# TODO: split into multiple methods to reduce complexity (35)
options = command_line.get_options_dict()
# Use stdout to output logs, only if no command line argument is
# provided
argc = len(sys.argv) - 1
if "-d" in sys.argv or "--debug" in sys.argv:
argc -= 1
if not argc:
# Switch back the log output to stderr (the default in Python)
# to avoid messing with any output from command line options.
# Use when targetting Python 3.7 minimum
# console_handler.setStream(sys.stderr)
# Until then...
logger.removeHandler(log.console_handler)
log.console_handler = logging.StreamHandler(stream=sys.stdout)
log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
logger.addHandler(log.console_handler)
# Set up logger
if options.contains("debug"):
log.console_handler.setFormatter(log.DEBUG_FORMATTER)
logger.setLevel(logging.DEBUG)
# Text only commands
# Print Lutris version and exit
if options.contains("version"):
executable_name = os.path.basename(sys.argv[0])
print(executable_name + "-" + settings.VERSION)
logger.setLevel(logging.NOTSET)
return 0
init_lutris()
migrate()
run_all_checks()
if options.contains("dest"):
dest_dir = options.lookup_value("dest").get_string()
else:
dest_dir = None
# List game
if options.contains("list-games"):
game_list = games_db.get_games()
if options.contains("installed"):
game_list = [game for game in game_list if game["installed"]]
if options.contains("json"):
self.print_game_json(command_line, game_list)
else:
self.print_game_list(command_line, game_list)
return 0
# List Steam games
if options.contains("list-steam-games"):
self.print_steam_list(command_line)
return 0
# List Steam folders
if options.contains("list-steam-folders"):
self.print_steam_folders(command_line)
return 0
# List Runners
if options.contains("list-runners"):
self.print_runners()
return 0
# List Wine Runners
if options.contains("list-wine-runners"):
self.print_wine_runners()
return 0
# install Runner
if options.contains("install-runner"):
runner = options.lookup_value("install-runner").get_string()
self.install_runner(runner)
return 0
# Uninstall Runner
if options.contains("uninstall-runner"):
runner = options.lookup_value("uninstall-runner").get_string()
self.uninstall_runner(runner)
return 0
if options.contains("export"):
slug = options.lookup_value("export").get_string()
if not dest_dir:
print("No destination dir given")
else:
export_game(slug, dest_dir)
return 0
if options.contains("import"):
filepath = options.lookup_value("import").get_string()
if not dest_dir:
print("No destination dir given")
else:
import_game(filepath, dest_dir)
return 0
# Execute command in Lutris context
if options.contains("exec"):
command = options.lookup_value("exec").get_string()
self.execute_command(command)
return 0
if options.contains("submit-issue"):
IssueReportWindow(application=self)
return 0
try:
url = options.lookup_value(GLib.OPTION_REMAINING)
installer_info = self.get_lutris_action(url)
except ValueError:
self._print(command_line, _("%s is not a valid URI") % url.get_strv())
return 1
game_slug = installer_info["game_slug"]
action = installer_info["action"]
service = installer_info["service"]
appid = installer_info["appid"]
if options.contains("output-script"):
action = "write-script"
revision = installer_info["revision"]
installer_file = None
if options.contains("install"):
installer_file = options.lookup_value("install").get_string()
if installer_file.startswith(("http:", "https:")):
try:
request = Request(installer_file).get()
except HTTPError:
self._print(command_line, _("Failed to download %s") % installer_file)
return 1
try:
headers = dict(request.response_headers)
file_name = headers["Content-Disposition"].split("=", 1)[-1]
except (KeyError, IndexError):
file_name = os.path.basename(installer_file)
file_path = os.path.join(tempfile.gettempdir(), file_name)
self._print(command_line, _("download {url} to {file} started").format(
url=installer_file, file=file_path))
with open(file_path, 'wb') as dest_file:
dest_file.write(request.content)
installer_file = file_path
action = "install"
else:
installer_file = os.path.abspath(installer_file)
action = "install"
if not os.path.isfile(installer_file):
self._print(command_line, _("No such file: %s") % installer_file)
return 1
db_game = None
if game_slug and not service:
if action == "rungameid":
# Force db_game to use game id
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "id")
elif action == "rungame":
# Force db_game to use game slug
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "slug")
elif action == "install":
# Installers can use game or installer slugs
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "slug") \
or games_db.get_game_by_field(game_slug, "installer_slug")
else:
# Dazed and confused, try anything that might works
db_game = (
games_db.get_game_by_field(game_slug, "id")
or games_db.get_game_by_field(game_slug, "slug")
or games_db.get_game_by_field(game_slug, "installer_slug")
)
# If reinstall flag is passed, force the action to install
if options.contains("reinstall"):
action = "install"
if action == "write-script":
if not db_game or not db_game["id"]:
logger.warning("No game provided to generate the script")
return 1
self.generate_script(db_game, options.lookup_value("output-script").get_string())
return 0
# Graphical commands
self.activate()
self.set_tray_icon()
if not action:
if db_game and db_game["installed"]:
# Game found but no action provided, ask what to do
dlg = InstallOrPlayDialog(db_game["name"])
if not dlg.action_confirmed:
action = None
elif dlg.action == "play":
action = "rungame"
elif dlg.action == "install":
action = "install"
elif game_slug or installer_file or service:
# No game found, default to install if a game_slug or
# installer_file is provided
action = "install"
if service:
service_game = ServiceGameCollection.get_game(service, appid)
if service_game:
service = get_enabled_services()[service]()
service.install(service_game)
return 0
if action == "install":
installers = get_installers(
game_slug=game_slug,
installer_file=installer_file,
revision=revision,
)
if installers:
self.show_installer_window(installers)
elif action in ("rungame", "rungameid"):
if not db_game or not db_game["id"]:
logger.warning("No game found in library")
if not self.window.is_visible():
self.do_shutdown()
return 0
game = Game(db_game["id"])
self.on_game_launch(game)
return 0
def on_game_launch(self, game):
game.launch()
return True # Return True to continue handling the emission hook
def on_game_start(self, game):
self.running_games.append(game)
if settings.read_setting("hide_client_on_game_start") == "True":
self.window.hide() # Hide launcher window
return True
def on_game_install(self, game):
"""Request installation of a game"""
if game.service and game.service != "lutris":
service = get_enabled_services()[game.service]()
db_game = ServiceGameCollection.get_game(service.id, game.appid)
if not db_game:
logger.error("Can't find %s for %s", game.name, service.name)
return True
try:
game_id = service.install(db_game)
except ValueError as e:
logger.debug(e)
game_id = None
if game_id:
game = Game(game_id)
game.launch()
return True
if not game.slug:
raise ValueError("Invalid game passed: %s" % game)
# return True
installers = get_installers(game_slug=game.slug)
if installers:
self.show_installer_window(installers)
else:
ErrorDialog(_("There is no installer available for %s.") % game.name, parent=self.window)
return True
def on_game_install_update(self, game):
service = get_enabled_services()[game.service]()
db_game = games_db.get_game_by_field(game.id, "id")
installers = service.get_update_installers(db_game)
if installers:
self.show_installer_window(installers, service, game.appid, is_update=True)
else:
ErrorDialog(_("No updates found"))
return True
def on_game_install_dlc(self, game):
service = get_enabled_services()[game.service]()
db_game = games_db.get_game_by_field(game.id, "id")
installers = service.get_dlc_installers(db_game)
if installers:
self.show_installer_window(installers, service, game.appid)
else:
ErrorDialog(_("No DLC found"))
return True
def get_running_game_ids(self):
ids = []
for i in range(self.running_games.get_n_items()):
game = self.running_games.get_item(i)
ids.append(str(game.id))
return ids
def get_game_by_id(self, game_id):
for i in range(self.running_games.get_n_items()):
game = self.running_games.get_item(i)
if str(game.id) == str(game_id):
return game
return None
def on_game_stop(self, game):
"""Callback to remove the game from the running games"""
ids = self.get_running_game_ids()
if str(game.id) in ids:
try:
self.running_games.remove(ids.index(str(game.id)))
except ValueError:
pass
else:
logger.warning("%s not in %s", game.id, ids)
game.emit("game-stopped")
if settings.read_setting("hide_client_on_game_start") == "True":
self.window.show() # Show launcher window
elif not self.window.is_visible():
if self.running_games.get_n_items() == 0:
self.quit()
return True
@staticmethod
def get_lutris_action(url):
installer_info = {"game_slug": None, "revision": None, "action": None, "service": None, "appid": None}
if url:
url = url.get_strv()
if url:
url = url[0]
installer_info = parse_installer_url(url)
if installer_info is False:
raise ValueError
return installer_info
def print_game_list(self, command_line, game_list):
for game in game_list:
self._print(
command_line,
"{:4} | {:<40} | {:<40} | {:<15} | {:<64}".format(
game["id"],
game["name"][:40],
game["slug"][:40],
game["runner"] or "-",
game["directory"] or "-",
),
)
def print_game_json(self, command_line, game_list):
games = [
{
"id": game["id"],
"slug": game["slug"],
"name": game["name"],
"runner": game["runner"],
"platform": game["platform"] or None,
"year": game["year"] or None,
"directory": game["directory"] or None,
"hidden": bool(game["hidden"]),
"playtime": (
str(timedelta(hours=game["playtime"]))
if game["playtime"] else None
),
"lastplayed": (
str(datetime.fromtimestamp(game["lastplayed"]))
if game["lastplayed"] else None
)
} for game in game_list
]
self._print(command_line, json.dumps(games, indent=2))
def print_steam_list(self, command_line):
steamapps_paths = get_steamapps_paths()
for path in steamapps_paths if steamapps_paths else []:
appmanifest_files = get_appmanifests(path)
for appmanifest_file in appmanifest_files:
appmanifest = AppManifest(os.path.join(path, appmanifest_file))
self._print(
command_line,
" {:8} | {:<60} | {}".format(
appmanifest.steamid,
appmanifest.name or "-",
", ".join(appmanifest.states),
),
)
@staticmethod
def execute_command(command):
"""Execute an arbitrary command in a Lutris context
with the runtime enabled and monitored by a MonitoredCommand
"""
Application.show_update_runtime_dialog()
logger.info("Running command '%s'", command)
monitored_command = exec_command(command)
try:
GLib.MainLoop().run()
except KeyboardInterrupt:
monitored_command.stop()
def print_steam_folders(self, command_line):
steamapps_paths = get_steamapps_paths()
for platform in ("linux", "windows"):
for path in steamapps_paths[platform] if steamapps_paths else []:
self._print(command_line, path)
def print_runners(self):
runnersName = get_runner_names()
sortednames = sorted(runnersName.keys(), key=lambda x: x.lower())
for name in sortednames:
print(name)
def print_wine_runners(self):
runnersName = get_runners("wine")
for i in runnersName["versions"]:
if i["version"]:
print(i)
def install_runner(self, runner):
if runner.startswith("lutris"):
self.install_wine_cli(runner)
else:
self.install_cli(runner)
def uninstall_runner(self, runner):
if "wine" in runner:
print("Are sure you want to delete Wine and all of the installed runners?[Y/N]")
ans = input()
if ans.lower() in ("y", "yes"):
self.uninstall_runner_cli(runner)
else:
print("Not Removing Wine")
elif runner.startswith("lutris"):
self.wine_runner_uninstall(runner)
else:
self.uninstall_runner_cli(runner)
def install_wine_cli(self, version):
"""
Downloads wine runner using lutris -r <runner>
"""
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
runner_path = os.path.join(WINE_DIR, f"{version}{'' if '-x86_64' in version else '-x86_64'}")
if os.path.isdir(runner_path):
print(f"Wine version '{version}' is already installed.")
else:
try:
runner = import_runner("wine")
runner().install(downloader=simple_downloader, version=version)
print(f"Wine version '{version}' has been installed.")
except (InvalidRunner, RunnerInstallationError) as ex:
print(ex.message)
def wine_runner_uninstall(self, version):
version = f"{version}{'' if '-x86_64' in version else '-x86_64'}"
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
runner_path = os.path.join(WINE_DIR, version)
if os.path.isdir(runner_path):
system.remove_folder(runner_path)
print(f"Wine version '{version}' has been removed.")
else:
print(f"""
Specified version of Wine is not installed: {version}.
Please check if the Wine Runner and specified version are installed (for that use --list-wine-runners).
Also, check that the version specified is in the correct format.
""")
def install_cli(self, runner_name):
"""
install the runner provided in prepare_runner_cli()
"""
runner_path = os.path.join(settings.RUNNER_DIR, runner_name)
if os.path.isdir(runner_path):
print(f"'{runner_name}' is already installed.")
else:
try:
runner = import_runner(runner_name)
runner().install(version=None, downloader=simple_downloader, callback=None)
print(f"'{runner_name}' has been installed")
except (InvalidRunner, RunnerInstallationError) as ex:
print(ex.message)
def uninstall_runner_cli(self, runner_name):
"""
uninstall the runner given in application file located in lutris/gui/application.py
provided using lutris -u <runner>
"""
try:
runner_class = import_runner(runner_name)
runner = runner_class()
except InvalidRunner:
logger.error("Failed to import Runner: %s", runner_name)
return
if not runner.is_installed():
print(f"Runner '{runner_name}' is not installed.")
return
if runner.can_uninstall():
runner.uninstall()
print(f"'{runner_name}' has been uninstalled.")
else:
print(f"Runner '{runner_name}' cannot be uninstalled.")
def do_shutdown(self): # pylint: disable=arguments-differ
logger.info("Shutting down Lutris")
if self.window:
settings.write_setting("selected_category", self.window.selected_category)
self.window.destroy()
Gtk.Application.do_shutdown(self)
def set_tray_icon(self):
"""Creates or destroys a tray icon for the application"""
active = settings.read_setting("show_tray_icon", default="false").lower() == "true"
if active and not self.tray:
self.tray = LutrisStatusIcon(application=self)
if self.tray:
self.tray.set_visible(active)
__init__(self)
special
¶
Source code in lutris/gui/application.py
def __init__(self):
super().__init__(
application_id="net.lutris.Lutris",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
)
GObject.add_emission_hook(Game, "game-launch", self.on_game_launch)
GObject.add_emission_hook(Game, "game-start", self.on_game_start)
GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
GObject.add_emission_hook(Game, "game-install", self.on_game_install)
GObject.add_emission_hook(Game, "game-install-update", self.on_game_install_update)
GObject.add_emission_hook(Game, "game-install-dlc", self.on_game_install_dlc)
GLib.set_application_name(_("Lutris"))
self.window = None
self.running_games = Gio.ListStore.new(Game)
self.app_windows = {}
self.tray = None
self.css_provider = Gtk.CssProvider.new()
self.run_in_background = False
self.style_manager = None
if os.geteuid() == 0:
ErrorDialog(_("Running Lutris as root is not recommended and may cause unexpected issues"))
try:
self.css_provider.load_from_path(os.path.join(datapath.get(), "ui", "lutris.css"))
except GLib.Error as e:
logger.exception(e)
if hasattr(self, "add_main_option"):
self.add_arguments()
else:
ErrorDialog(_("Your Linux distribution is too old. Lutris won't function properly."))
add_arguments(self)
¶
Source code in lutris/gui/application.py
def add_arguments(self):
if hasattr(self, "set_option_context_summary"):
self.set_option_context_summary(_(
"Run a game directly by adding the parameter lutris:rungame/game-identifier.\n"
"If several games share the same identifier you can use the numerical ID "
"(displayed when running lutris --list-games) and add "
"lutris:rungameid/numerical-id.\n"
"To install a game, add lutris:install/game-identifier."
))
else:
logger.warning("GLib.set_option_context_summary missing, " "was added in GLib 2.56 (Released 2018-03-12)")
self.add_main_option(
"version",
ord("v"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Print the version of Lutris and exit"),
None,
)
self.add_main_option(
"debug",
ord("d"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Show debug messages"),
None,
)
self.add_main_option(
"install",
ord("i"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Install a game from a yml file"),
None,
)
self.add_main_option(
"output-script",
ord("b"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Generate a bash script to run a game without the client"),
None,
)
self.add_main_option(
"exec",
ord("e"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Execute a program with the Lutris Runtime"),
None,
)
self.add_main_option(
"list-games",
ord("l"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all games in database"),
None,
)
self.add_main_option(
"installed",
ord("o"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Only list installed games"),
None,
)
self.add_main_option(
"list-steam-games",
ord("s"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List available Steam games"),
None,
)
self.add_main_option(
"list-steam-folders",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known Steam library folders"),
None,
)
self.add_main_option(
"list-runners",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known runners"),
None,
)
self.add_main_option(
"list-wine-versions",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("List all known Wine versions"),
None,
)
self.add_main_option(
"install-runner",
ord("r"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Install a Runner"),
None,
)
self.add_main_option(
"uninstall-runner",
ord("u"),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Uninstall a Runner"),
None,
)
self.add_main_option(
"export",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Export a game"),
None,
)
self.add_main_option(
"import",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Import a game"),
None,
)
self.add_main_option(
"dest",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_("Destination path for export"),
None,
)
self.add_main_option(
"json",
ord("j"),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Display the list of games in JSON format"),
None,
)
self.add_main_option(
"reinstall",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_("Reinstall game"),
None,
)
self.add_main_option("submit-issue", 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Submit an issue"), None)
self.add_main_option(
GLib.OPTION_REMAINING,
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING_ARRAY,
_("URI to open"),
"URI",
)
do_activate(self)
¶
activate(self)
Source code in lutris/gui/application.py
def do_activate(self): # pylint: disable=arguments-differ
Application.show_update_runtime_dialog()
if not self.window:
self.window = LutrisWindow(application=self)
screen = self.window.props.screen # pylint: disable=no-member
Gtk.StyleContext.add_provider_for_screen(screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
if not self.run_in_background:
self.window.present()
else:
# Reset run in background to False. Future calls will set it
# accordingly
self.run_in_background = False
do_command_line(self, command_line)
¶
command_line(self, command_line:Gio.ApplicationCommandLine) -> int
Source code in lutris/gui/application.py
def do_command_line(self, command_line): # noqa: C901 # pylint: disable=arguments-differ
# pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
# TODO: split into multiple methods to reduce complexity (35)
options = command_line.get_options_dict()
# Use stdout to output logs, only if no command line argument is
# provided
argc = len(sys.argv) - 1
if "-d" in sys.argv or "--debug" in sys.argv:
argc -= 1
if not argc:
# Switch back the log output to stderr (the default in Python)
# to avoid messing with any output from command line options.
# Use when targetting Python 3.7 minimum
# console_handler.setStream(sys.stderr)
# Until then...
logger.removeHandler(log.console_handler)
log.console_handler = logging.StreamHandler(stream=sys.stdout)
log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
logger.addHandler(log.console_handler)
# Set up logger
if options.contains("debug"):
log.console_handler.setFormatter(log.DEBUG_FORMATTER)
logger.setLevel(logging.DEBUG)
# Text only commands
# Print Lutris version and exit
if options.contains("version"):
executable_name = os.path.basename(sys.argv[0])
print(executable_name + "-" + settings.VERSION)
logger.setLevel(logging.NOTSET)
return 0
init_lutris()
migrate()
run_all_checks()
if options.contains("dest"):
dest_dir = options.lookup_value("dest").get_string()
else:
dest_dir = None
# List game
if options.contains("list-games"):
game_list = games_db.get_games()
if options.contains("installed"):
game_list = [game for game in game_list if game["installed"]]
if options.contains("json"):
self.print_game_json(command_line, game_list)
else:
self.print_game_list(command_line, game_list)
return 0
# List Steam games
if options.contains("list-steam-games"):
self.print_steam_list(command_line)
return 0
# List Steam folders
if options.contains("list-steam-folders"):
self.print_steam_folders(command_line)
return 0
# List Runners
if options.contains("list-runners"):
self.print_runners()
return 0
# List Wine Runners
if options.contains("list-wine-runners"):
self.print_wine_runners()
return 0
# install Runner
if options.contains("install-runner"):
runner = options.lookup_value("install-runner").get_string()
self.install_runner(runner)
return 0
# Uninstall Runner
if options.contains("uninstall-runner"):
runner = options.lookup_value("uninstall-runner").get_string()
self.uninstall_runner(runner)
return 0
if options.contains("export"):
slug = options.lookup_value("export").get_string()
if not dest_dir:
print("No destination dir given")
else:
export_game(slug, dest_dir)
return 0
if options.contains("import"):
filepath = options.lookup_value("import").get_string()
if not dest_dir:
print("No destination dir given")
else:
import_game(filepath, dest_dir)
return 0
# Execute command in Lutris context
if options.contains("exec"):
command = options.lookup_value("exec").get_string()
self.execute_command(command)
return 0
if options.contains("submit-issue"):
IssueReportWindow(application=self)
return 0
try:
url = options.lookup_value(GLib.OPTION_REMAINING)
installer_info = self.get_lutris_action(url)
except ValueError:
self._print(command_line, _("%s is not a valid URI") % url.get_strv())
return 1
game_slug = installer_info["game_slug"]
action = installer_info["action"]
service = installer_info["service"]
appid = installer_info["appid"]
if options.contains("output-script"):
action = "write-script"
revision = installer_info["revision"]
installer_file = None
if options.contains("install"):
installer_file = options.lookup_value("install").get_string()
if installer_file.startswith(("http:", "https:")):
try:
request = Request(installer_file).get()
except HTTPError:
self._print(command_line, _("Failed to download %s") % installer_file)
return 1
try:
headers = dict(request.response_headers)
file_name = headers["Content-Disposition"].split("=", 1)[-1]
except (KeyError, IndexError):
file_name = os.path.basename(installer_file)
file_path = os.path.join(tempfile.gettempdir(), file_name)
self._print(command_line, _("download {url} to {file} started").format(
url=installer_file, file=file_path))
with open(file_path, 'wb') as dest_file:
dest_file.write(request.content)
installer_file = file_path
action = "install"
else:
installer_file = os.path.abspath(installer_file)
action = "install"
if not os.path.isfile(installer_file):
self._print(command_line, _("No such file: %s") % installer_file)
return 1
db_game = None
if game_slug and not service:
if action == "rungameid":
# Force db_game to use game id
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "id")
elif action == "rungame":
# Force db_game to use game slug
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "slug")
elif action == "install":
# Installers can use game or installer slugs
self.run_in_background = True
db_game = games_db.get_game_by_field(game_slug, "slug") \
or games_db.get_game_by_field(game_slug, "installer_slug")
else:
# Dazed and confused, try anything that might works
db_game = (
games_db.get_game_by_field(game_slug, "id")
or games_db.get_game_by_field(game_slug, "slug")
or games_db.get_game_by_field(game_slug, "installer_slug")
)
# If reinstall flag is passed, force the action to install
if options.contains("reinstall"):
action = "install"
if action == "write-script":
if not db_game or not db_game["id"]:
logger.warning("No game provided to generate the script")
return 1
self.generate_script(db_game, options.lookup_value("output-script").get_string())
return 0
# Graphical commands
self.activate()
self.set_tray_icon()
if not action:
if db_game and db_game["installed"]:
# Game found but no action provided, ask what to do
dlg = InstallOrPlayDialog(db_game["name"])
if not dlg.action_confirmed:
action = None
elif dlg.action == "play":
action = "rungame"
elif dlg.action == "install":
action = "install"
elif game_slug or installer_file or service:
# No game found, default to install if a game_slug or
# installer_file is provided
action = "install"
if service:
service_game = ServiceGameCollection.get_game(service, appid)
if service_game:
service = get_enabled_services()[service]()
service.install(service_game)
return 0
if action == "install":
installers = get_installers(
game_slug=game_slug,
installer_file=installer_file,
revision=revision,
)
if installers:
self.show_installer_window(installers)
elif action in ("rungame", "rungameid"):
if not db_game or not db_game["id"]:
logger.warning("No game found in library")
if not self.window.is_visible():
self.do_shutdown()
return 0
game = Game(db_game["id"])
self.on_game_launch(game)
return 0
do_shutdown(self)
¶
shutdown(self)
Source code in lutris/gui/application.py
def do_shutdown(self): # pylint: disable=arguments-differ
logger.info("Shutting down Lutris")
if self.window:
settings.write_setting("selected_category", self.window.selected_category)
self.window.destroy()
Gtk.Application.do_shutdown(self)
do_startup(self)
¶
Sets up the application on first start.
Source code in lutris/gui/application.py
def do_startup(self): # pylint: disable=arguments-differ
"""Sets up the application on first start."""
Gtk.Application.do_startup(self)
signal.signal(signal.SIGINT, signal.SIG_DFL)
action = Gio.SimpleAction.new("quit")
action.connect("activate", lambda *x: self.quit())
self.add_action(action)
self.add_accelerator("<Primary>q", "app.quit")
self.style_manager = StyleManager()
execute_command(command)
staticmethod
¶
Execute an arbitrary command in a Lutris context with the runtime enabled and monitored by a MonitoredCommand
Source code in lutris/gui/application.py
@staticmethod
def execute_command(command):
"""Execute an arbitrary command in a Lutris context
with the runtime enabled and monitored by a MonitoredCommand
"""
Application.show_update_runtime_dialog()
logger.info("Running command '%s'", command)
monitored_command = exec_command(command)
try:
GLib.MainLoop().run()
except KeyboardInterrupt:
monitored_command.stop()
generate_script(self, db_game, script_path)
¶
Output a script to a file. The script is capable of launching a game without the client
Source code in lutris/gui/application.py
def generate_script(self, db_game, script_path):
"""Output a script to a file.
The script is capable of launching a game without the client
"""
game = Game(db_game["id"])
game.load_config()
game.write_script(script_path)
get_game_by_id(self, game_id)
¶
Source code in lutris/gui/application.py
def get_game_by_id(self, game_id):
for i in range(self.running_games.get_n_items()):
game = self.running_games.get_item(i)
if str(game.id) == str(game_id):
return game
return None
get_lutris_action(url)
staticmethod
¶
Source code in lutris/gui/application.py
@staticmethod
def get_lutris_action(url):
installer_info = {"game_slug": None, "revision": None, "action": None, "service": None, "appid": None}
if url:
url = url.get_strv()
if url:
url = url[0]
installer_info = parse_installer_url(url)
if installer_info is False:
raise ValueError
return installer_info
get_running_game_ids(self)
¶
Source code in lutris/gui/application.py
def get_running_game_ids(self):
ids = []
for i in range(self.running_games.get_n_items()):
game = self.running_games.get_item(i)
ids.append(str(game.id))
return ids
get_window_key(self, **kwargs)
¶
Source code in lutris/gui/application.py
def get_window_key(self, **kwargs):
if kwargs.get("appid"):
return kwargs["appid"]
if kwargs.get("runner"):
return kwargs["runner"].name
if kwargs.get("installers"):
return kwargs["installers"][0]["game_slug"]
if kwargs.get("game"):
return str(kwargs["game"].id)
return str(kwargs)
install_cli(self, runner_name)
¶
install the runner provided in prepare_runner_cli()
Source code in lutris/gui/application.py
def install_cli(self, runner_name):
"""
install the runner provided in prepare_runner_cli()
"""
runner_path = os.path.join(settings.RUNNER_DIR, runner_name)
if os.path.isdir(runner_path):
print(f"'{runner_name}' is already installed.")
else:
try:
runner = import_runner(runner_name)
runner().install(version=None, downloader=simple_downloader, callback=None)
print(f"'{runner_name}' has been installed")
except (InvalidRunner, RunnerInstallationError) as ex:
print(ex.message)
install_runner(self, runner)
¶
Source code in lutris/gui/application.py
def install_runner(self, runner):
if runner.startswith("lutris"):
self.install_wine_cli(runner)
else:
self.install_cli(runner)
install_wine_cli(self, version)
¶
Downloads wine runner using lutris -r
Source code in lutris/gui/application.py
def install_wine_cli(self, version):
"""
Downloads wine runner using lutris -r <runner>
"""
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
runner_path = os.path.join(WINE_DIR, f"{version}{'' if '-x86_64' in version else '-x86_64'}")
if os.path.isdir(runner_path):
print(f"Wine version '{version}' is already installed.")
else:
try:
runner = import_runner("wine")
runner().install(downloader=simple_downloader, version=version)
print(f"Wine version '{version}' has been installed.")
except (InvalidRunner, RunnerInstallationError) as ex:
print(ex.message)
on_app_window_destroyed(self, app_window, window_key)
¶
Remove the reference to the window when it has been destroyed
Source code in lutris/gui/application.py
def on_app_window_destroyed(self, app_window, window_key):
"""Remove the reference to the window when it has been destroyed"""
window_key = str(app_window.__class__.__name__) + window_key
try:
del self.app_windows[window_key]
logger.debug("Removed window %s", window_key)
except KeyError:
logger.warning("Failed to remove window %s", window_key)
logger.info("Available windows: %s", ", ".join(self.app_windows.keys()))
return True
on_game_install(self, game)
¶
Request installation of a game
Source code in lutris/gui/application.py
def on_game_install(self, game):
"""Request installation of a game"""
if game.service and game.service != "lutris":
service = get_enabled_services()[game.service]()
db_game = ServiceGameCollection.get_game(service.id, game.appid)
if not db_game:
logger.error("Can't find %s for %s", game.name, service.name)
return True
try:
game_id = service.install(db_game)
except ValueError as e:
logger.debug(e)
game_id = None
if game_id:
game = Game(game_id)
game.launch()
return True
if not game.slug:
raise ValueError("Invalid game passed: %s" % game)
# return True
installers = get_installers(game_slug=game.slug)
if installers:
self.show_installer_window(installers)
else:
ErrorDialog(_("There is no installer available for %s.") % game.name, parent=self.window)
return True
on_game_install_dlc(self, game)
¶
Source code in lutris/gui/application.py
def on_game_install_dlc(self, game):
service = get_enabled_services()[game.service]()
db_game = games_db.get_game_by_field(game.id, "id")
installers = service.get_dlc_installers(db_game)
if installers:
self.show_installer_window(installers, service, game.appid)
else:
ErrorDialog(_("No DLC found"))
return True
on_game_install_update(self, game)
¶
Source code in lutris/gui/application.py
def on_game_install_update(self, game):
service = get_enabled_services()[game.service]()
db_game = games_db.get_game_by_field(game.id, "id")
installers = service.get_update_installers(db_game)
if installers:
self.show_installer_window(installers, service, game.appid, is_update=True)
else:
ErrorDialog(_("No updates found"))
return True
on_game_launch(self, game)
¶
Source code in lutris/gui/application.py
def on_game_launch(self, game):
game.launch()
return True # Return True to continue handling the emission hook
on_game_start(self, game)
¶
Source code in lutris/gui/application.py
def on_game_start(self, game):
self.running_games.append(game)
if settings.read_setting("hide_client_on_game_start") == "True":
self.window.hide() # Hide launcher window
return True
on_game_stop(self, game)
¶
Callback to remove the game from the running games
Source code in lutris/gui/application.py
def on_game_stop(self, game):
"""Callback to remove the game from the running games"""
ids = self.get_running_game_ids()
if str(game.id) in ids:
try:
self.running_games.remove(ids.index(str(game.id)))
except ValueError:
pass
else:
logger.warning("%s not in %s", game.id, ids)
game.emit("game-stopped")
if settings.read_setting("hide_client_on_game_start") == "True":
self.window.show() # Show launcher window
elif not self.window.is_visible():
if self.running_games.get_n_items() == 0:
self.quit()
return True
print_game_json(self, command_line, game_list)
¶
Source code in lutris/gui/application.py
def print_game_json(self, command_line, game_list):
games = [
{
"id": game["id"],
"slug": game["slug"],
"name": game["name"],
"runner": game["runner"],
"platform": game["platform"] or None,
"year": game["year"] or None,
"directory": game["directory"] or None,
"hidden": bool(game["hidden"]),
"playtime": (
str(timedelta(hours=game["playtime"]))
if game["playtime"] else None
),
"lastplayed": (
str(datetime.fromtimestamp(game["lastplayed"]))
if game["lastplayed"] else None
)
} for game in game_list
]
self._print(command_line, json.dumps(games, indent=2))
print_game_list(self, command_line, game_list)
¶
Source code in lutris/gui/application.py
def print_game_list(self, command_line, game_list):
for game in game_list:
self._print(
command_line,
"{:4} | {:<40} | {:<40} | {:<15} | {:<64}".format(
game["id"],
game["name"][:40],
game["slug"][:40],
game["runner"] or "-",
game["directory"] or "-",
),
)
print_runners(self)
¶
Source code in lutris/gui/application.py
def print_runners(self):
runnersName = get_runner_names()
sortednames = sorted(runnersName.keys(), key=lambda x: x.lower())
for name in sortednames:
print(name)
print_steam_folders(self, command_line)
¶
Source code in lutris/gui/application.py
def print_steam_folders(self, command_line):
steamapps_paths = get_steamapps_paths()
for platform in ("linux", "windows"):
for path in steamapps_paths[platform] if steamapps_paths else []:
self._print(command_line, path)
print_steam_list(self, command_line)
¶
Source code in lutris/gui/application.py
def print_steam_list(self, command_line):
steamapps_paths = get_steamapps_paths()
for path in steamapps_paths if steamapps_paths else []:
appmanifest_files = get_appmanifests(path)
for appmanifest_file in appmanifest_files:
appmanifest = AppManifest(os.path.join(path, appmanifest_file))
self._print(
command_line,
" {:8} | {:<60} | {}".format(
appmanifest.steamid,
appmanifest.name or "-",
", ".join(appmanifest.states),
),
)
print_wine_runners(self)
¶
Source code in lutris/gui/application.py
def print_wine_runners(self):
runnersName = get_runners("wine")
for i in runnersName["versions"]:
if i["version"]:
print(i)
set_tray_icon(self)
¶
Creates or destroys a tray icon for the application
Source code in lutris/gui/application.py
def set_tray_icon(self):
"""Creates or destroys a tray icon for the application"""
active = settings.read_setting("show_tray_icon", default="false").lower() == "true"
if active and not self.tray:
self.tray = LutrisStatusIcon(application=self)
if self.tray:
self.tray.set_visible(active)
show_installer_window(self, installers, service=None, appid=None, is_update=False)
¶
Source code in lutris/gui/application.py
def show_installer_window(self, installers, service=None, appid=None, is_update=False):
self.show_window(
InstallerWindow,
installers=installers,
service=service,
appid=appid,
is_update=is_update
)
show_update_runtime_dialog()
staticmethod
¶
Source code in lutris/gui/application.py
@staticmethod
def show_update_runtime_dialog():
if os.environ.get("LUTRIS_SKIP_INIT"):
logger.debug("Skipping initialization")
else:
init_dialog = LutrisInitDialog(update_runtime)
init_dialog.run()
show_window(self, window_class, **kwargs)
¶
Instanciate a window keeping 1 instance max
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
window_class |
Gtk.Window |
class to create the instance from |
required |
kwargs |
dict |
Additional arguments to pass to the instanciated window |
{} |
Returns:
| Type | Description |
|---|---|
Gtk.Window |
the existing window instance or a newly created one |
Source code in lutris/gui/application.py
def show_window(self, window_class, **kwargs):
"""Instanciate a window keeping 1 instance max
Params:
window_class (Gtk.Window): class to create the instance from
kwargs (dict): Additional arguments to pass to the instanciated window
Returns:
Gtk.Window: the existing window instance or a newly created one
"""
window_key = str(window_class.__name__) + self.get_window_key(**kwargs)
if self.app_windows.get(window_key):
self.app_windows[window_key].present()
return self.app_windows[window_key]
if issubclass(window_class, Gtk.Dialog):
if "parent" in kwargs:
window_inst = window_class(**kwargs)
else:
window_inst = window_class(parent=self.window, **kwargs)
window_inst.set_application(self)
else:
window_inst = window_class(application=self, **kwargs)
window_inst.connect("destroy", self.on_app_window_destroyed, self.get_window_key(**kwargs))
self.app_windows[window_key] = window_inst
logger.debug("Showing window %s", window_key)
window_inst.show()
return window_inst
uninstall_runner(self, runner)
¶
Source code in lutris/gui/application.py
def uninstall_runner(self, runner):
if "wine" in runner:
print("Are sure you want to delete Wine and all of the installed runners?[Y/N]")
ans = input()
if ans.lower() in ("y", "yes"):
self.uninstall_runner_cli(runner)
else:
print("Not Removing Wine")
elif runner.startswith("lutris"):
self.wine_runner_uninstall(runner)
else:
self.uninstall_runner_cli(runner)
uninstall_runner_cli(self, runner_name)
¶
uninstall the runner given in application file located in lutris/gui/application.py
provided using lutris -u
Source code in lutris/gui/application.py
def uninstall_runner_cli(self, runner_name):
"""
uninstall the runner given in application file located in lutris/gui/application.py
provided using lutris -u <runner>
"""
try:
runner_class = import_runner(runner_name)
runner = runner_class()
except InvalidRunner:
logger.error("Failed to import Runner: %s", runner_name)
return
if not runner.is_installed():
print(f"Runner '{runner_name}' is not installed.")
return
if runner.can_uninstall():
runner.uninstall()
print(f"'{runner_name}' has been uninstalled.")
else:
print(f"Runner '{runner_name}' cannot be uninstalled.")
wine_runner_uninstall(self, version)
¶
Source code in lutris/gui/application.py
def wine_runner_uninstall(self, version):
version = f"{version}{'' if '-x86_64' in version else '-x86_64'}"
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
runner_path = os.path.join(WINE_DIR, version)
if os.path.isdir(runner_path):
system.remove_folder(runner_path)
print(f"Wine version '{version}' has been removed.")
else:
print(f"""
Specified version of Wine is not installed: {version}.
Please check if the Wine Runner and specified version are installed (for that use --list-wine-runners).
Also, check that the version specified is in the correct format.
""")
config
special
¶
DIALOG_HEIGHT
¶
DIALOG_WIDTH
¶
add_game
¶
AddGameDialog (GameDialogCommon)
¶
Add game dialog class.
Source code in lutris/gui/config/add_game.py
class AddGameDialog(GameDialogCommon):
"""Add game dialog class."""
def __init__(self, parent, game=None, runner=None):
super().__init__(_("Add a new game"), parent=parent)
self.game = game
self.saved = False
if game:
self.runner_name = game.runner_name
self.slug = game.slug
else:
self.runner_name = runner
self.slug = None
self.lutris_config = LutrisConfig(
runner_slug=self.runner_name,
level="game",
)
self.build_notebook()
self.build_tabs("game")
self.build_action_area(self.on_save)
self.name_entry.grab_focus()
self.connect("delete-event", self.on_cancel_clicked)
self.show_all()
__init__(self, parent, game=None, runner=None)
special
¶
Source code in lutris/gui/config/add_game.py
def __init__(self, parent, game=None, runner=None):
super().__init__(_("Add a new game"), parent=parent)
self.game = game
self.saved = False
if game:
self.runner_name = game.runner_name
self.slug = game.slug
else:
self.runner_name = runner
self.slug = None
self.lutris_config = LutrisConfig(
runner_slug=self.runner_name,
level="game",
)
self.build_notebook()
self.build_tabs("game")
self.build_action_area(self.on_save)
self.name_entry.grab_focus()
self.connect("delete-event", self.on_cancel_clicked)
self.show_all()
base_config_box
¶
BaseConfigBox (VBox)
¶
Source code in lutris/gui/config/base_config_box.py
class BaseConfigBox(VBox):
def get_section_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup("<b>%s</b>" % text)
label.set_alignment(0, 0.5)
label.set_margin_bottom(8)
return label
def get_description_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup("%s" % text)
label.set_line_wrap(True)
label.set_alignment(0, 0.5)
return label
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(50)
self.set_margin_bottom(50)
self.set_margin_right(80)
self.set_margin_left(80)
__init__(self)
special
¶
Source code in lutris/gui/config/base_config_box.py
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(50)
self.set_margin_bottom(50)
self.set_margin_right(80)
self.set_margin_left(80)
get_description_label(self, text)
¶
Source code in lutris/gui/config/base_config_box.py
def get_description_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup("%s" % text)
label.set_line_wrap(True)
label.set_alignment(0, 0.5)
return label
get_section_label(self, text)
¶
Source code in lutris/gui/config/base_config_box.py
def get_section_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup("<b>%s</b>" % text)
label.set_alignment(0, 0.5)
label.set_margin_bottom(8)
return label
boxes
¶
Widget generators and their signal handlers
ConfigBox (VBox)
¶
Dynamically generate a vbox built upon on a python dict.
Source code in lutris/gui/config/boxes.py
class ConfigBox(VBox):
"""Dynamically generate a vbox built upon on a python dict."""
def __init__(self, game=None):
super().__init__()
self.options = []
self.game = game
self.config = None
self.raw_config = None
self.option_widget = None
self.wrapper = None
self.tooltip_default = None
self.files = []
self.files_list_store = None
def generate_top_info_box(self, text):
"""Add a top section with general help text for the current tab"""
help_box = Gtk.Box()
help_box.set_margin_left(15)
help_box.set_margin_right(15)
help_box.set_margin_bottom(5)
icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU)
help_box.pack_start(icon, False, False, 5)
title_label = Gtk.Label("<i>%s</i>" % text)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_use_markup(True)
help_box.pack_start(title_label, False, False, 5)
self.pack_start(help_box, False, False, 0)
self.pack_start(Gtk.HSeparator(), False, False, 12)
help_box.show_all()
def generate_widgets(self, config_section): # noqa: C901 # pylint: disable=too-many-branches,too-many-statements
"""Parse the config dict and generates widget accordingly."""
if not self.options:
no_options_label = Label(_("No options available"))
no_options_label.set_halign(Gtk.Align.CENTER)
no_options_label.set_valign(Gtk.Align.CENTER)
self.pack_start(no_options_label, True, True, 0)
return
# Select config section.
if config_section == "game":
self.config = self.lutris_config.game_config
self.raw_config = self.lutris_config.raw_game_config
elif config_section == "runner":
self.config = self.lutris_config.runner_config
self.raw_config = self.lutris_config.raw_runner_config
elif config_section == "system":
self.config = self.lutris_config.system_config
self.raw_config = self.lutris_config.raw_system_config
# Go thru all options.
for option in self.options:
if "scope" in option:
if config_section not in option["scope"]:
continue
option_key = option["option"]
value = self.config.get(option_key)
default = option.get("default")
if callable(option.get("choices")) and option["type"] != "choice_with_search":
option["choices"] = option["choices"]()
if callable(option.get("condition")):
option["condition"] = option["condition"]()
self.wrapper = Gtk.Box()
self.wrapper.set_spacing(12)
self.wrapper.set_margin_bottom(6)
# Set tooltip's "Default" part
default = option.get("default")
self.tooltip_default = default if isinstance(default, str) else None
# Generate option widget
self.option_widget = None
self.call_widget_generator(option, option_key, value, default)
# Reset button
reset_btn = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
reset_btn.set_relief(Gtk.ReliefStyle.NONE)
reset_btn.set_tooltip_text(_("Reset option to global or default config"))
reset_btn.connect(
"clicked",
self.on_reset_button_clicked,
option,
self.option_widget,
self.wrapper,
)
placeholder = Gtk.Box()
placeholder.set_size_request(32, 32)
if option_key not in self.raw_config:
reset_btn.set_visible(False)
reset_btn.set_no_show_all(True)
placeholder.pack_start(reset_btn, False, False, 0)
# Tooltip
helptext = option.get("help")
if isinstance(self.tooltip_default, str):
helptext = helptext + "\n\n" if helptext else ""
helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
if value != default and option_key not in self.raw_config:
helptext = helptext + "\n\n" if helptext else ""
helptext += _(
"<i>(Italic indicates that this option is "
"modified in a lower configuration level.)</i>"
)
if helptext:
self.wrapper.props.has_tooltip = True
self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)
hbox = Gtk.Box()
hbox.set_margin_left(18)
hbox.pack_end(placeholder, False, False, 5)
# Grey out option if condition unmet
if "condition" in option and not option["condition"]:
hbox.set_sensitive(False)
# Hide if advanced
if option.get("advanced"):
hbox.get_style_context().add_class("advanced")
show_advanced = settings.read_setting("show_advanced_options")
if show_advanced != "True":
hbox.set_no_show_all(True)
hbox.pack_start(self.wrapper, True, True, 0)
self.pack_start(hbox, False, False, 0)
def call_widget_generator(self, option, option_key, value, default): # noqa: C901
"""Call the right generation method depending on option type."""
# pylint: disable=too-many-branches
option_type = option["type"]
option_size = option.get("size", None)
if option_key in self.raw_config:
self.set_style_property("font-weight", "bold", self.wrapper)
elif value != default:
self.set_style_property("font-style", "italic", self.wrapper)
if option_type == "choice":
self.generate_combobox(option_key, option["choices"], option["label"], value, default)
elif option_type == "choice_with_entry":
self.generate_combobox(
option_key,
option["choices"],
option["label"],
value,
default,
has_entry=True,
)
elif option_type == "choice_with_search":
self.generate_searchable_combobox(
option_key,
option["choices"],
option["label"],
value,
default,
)
elif option_type == "bool":
self.generate_checkbox(option, value)
self.tooltip_default = "Enabled" if default else "Disabled"
elif option_type == "extended_bool":
self.generate_checkbox_with_callback(option, value)
self.tooltip_default = "Enabled" if default else "Disabled"
elif option_type == "range":
self.generate_range(option_key, option["min"], option["max"], option["label"], value)
elif option_type == "string":
if "label" not in option:
raise ValueError("Option %s has no label" % option)
self.generate_entry(option_key, option["label"], value, option_size)
elif option_type == "directory_chooser":
self.generate_directory_chooser(option, value)
elif option_type == "file":
self.generate_file_chooser(option, value)
elif option_type == "multiple":
self.generate_multiple_file_chooser(option_key, option["label"], value)
elif option_type == "label":
self.generate_label(option["label"])
elif option_type == "mapping":
self.generate_editable_grid(option_key, label=option["label"], value=value)
else:
raise ValueError("Unknown widget type %s" % option_type)
# Label
def generate_label(self, text):
"""Generate a simple label."""
label = Label(text)
label.set_use_markup(True)
label.set_halign(Gtk.Align.START)
label.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, True, True, 0)
# Checkbox
def generate_checkbox(self, option, value=None):
"""Generate a checkbox."""
label = Label(option["label"])
self.wrapper.pack_start(label, False, False, 0)
switch = Gtk.Switch()
if value is True:
switch.set_active(value)
switch.connect("notify::active", self.checkbox_toggle, option["option"])
switch.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(switch, False, False, 0)
self.option_widget = switch
# Checkbox with callback
def generate_checkbox_with_callback(self, option, value=None):
"""Generate a checkbox. With callback"""
label = Label(option["label"])
self.wrapper.pack_start(label, False, False, 0)
checkbox = Gtk.Switch()
checkbox.set_sensitive(option["active"] is True)
if value is True:
checkbox.set_active(value)
checkbox.connect("notify::active", self._on_toggle_with_callback, option)
checkbox.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(checkbox, False, False, 0)
self.option_widget = checkbox
def checkbox_toggle(self, widget, _gparam, option_name):
"""Action for the checkbox's toggled signal."""
self.option_changed(widget, option_name, widget.get_active())
def _on_toggle_with_callback(self, widget, _gparam, option):
"""Action for the checkbox's toggled signal. With callback method"""
option_name = option["option"]
callback = option["callback"]
callback_on = option.get("callback_on")
if widget.get_active() == callback_on or callback_on is None:
AsyncCall(callback, self._on_callback_finished, widget, option, self.config)
else:
self.option_changed(widget, option_name, widget.get_active())
def _on_callback_finished(self, result, _error):
widget, option, response = result
if response:
self.option_changed(widget, option["option"], widget.get_active())
else:
widget.set_active(False)
# Entry
def generate_entry(self, option_name, label, value=None, option_size=None):
"""Generate an entry box."""
label = Label(label)
self.wrapper.pack_start(label, False, False, 0)
entry = Gtk.Entry()
if value:
entry.set_text(value)
entry.connect("changed", self.entry_changed, option_name)
expand = option_size != "small"
self.wrapper.pack_start(entry, expand, expand, 0)
self.option_widget = entry
def entry_changed(self, entry, option_name):
"""Action triggered for entry 'changed' signal."""
self.option_changed(entry, option_name, entry.get_text())
def generate_searchable_combobox(self, option_name, choice_func, label, value, default):
"""Generate a searchable combo box"""
combobox = SearchableCombobox(choice_func, value or default)
combobox.connect("changed", self.on_searchable_entry_changed, option_name)
self.wrapper.pack_start(Label(label), False, False, 0)
self.wrapper.pack_start(combobox, True, True, 0)
self.option_widget = combobox
def on_searchable_entry_changed(self, combobox, value, key):
self.option_changed(combobox, key, value)
def _populate_combobox_choices(self, liststore, choices, default):
for choice in choices:
if isinstance(choice, str):
choice = (choice, choice)
if choice[1] == default:
liststore.append((_("%s (default)") % choice[0], choice[1]))
self.tooltip_default = choice[0]
else:
liststore.append(choice)
# ComboBox
def generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False):
"""Generate a combobox (drop-down menu)."""
liststore = Gtk.ListStore(str, str)
self._populate_combobox_choices(liststore, choices, default)
# With entry ("choice_with_entry" type)
if has_entry:
combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
combobox.set_entry_text_column(0)
if value:
combobox.get_child().set_text(value)
# No entry ("choice" type)
else:
combobox = Gtk.ComboBox.new_with_model(liststore)
cell = Gtk.CellRendererText()
combobox.pack_start(cell, True)
combobox.add_attribute(cell, "text", 0)
combobox.set_id_column(1)
choices = list(v for k, v in choices)
if value in choices:
combobox.set_active_id(value)
else:
combobox.set_active_id(default)
combobox.connect("changed", self.on_combobox_change, option_name)
combobox.connect("scroll-event", self._on_combobox_scroll)
label = Label(label)
combobox.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(combobox, True, True, 0)
self.option_widget = combobox
@staticmethod
def _on_combobox_scroll(combobox, _event):
"""Prevents users from accidentally changing configuration values
while scrolling down dialogs.
"""
combobox.stop_emission_by_name("scroll-event")
return False
def on_combobox_change(self, combobox, option):
"""Action triggered on combobox 'changed' signal."""
list_store = combobox.get_model()
active = combobox.get_active()
option_value = None
if active < 0:
if combobox.get_has_entry():
option_value = combobox.get_child().get_text()
else:
option_value = list_store[active][1]
self.option_changed(combobox, option, option_value)
# Range
def generate_range(self, option_name, min_val, max_val, label, value=None):
"""Generate a ranged spin button."""
adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0)
spin_button = Gtk.SpinButton()
spin_button.set_adjustment(adjustment)
if value:
spin_button.set_value(value)
spin_button.connect("changed", self.on_spin_button_changed, option_name)
label = Label(label)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(spin_button, True, True, 0)
self.option_widget = spin_button
def on_spin_button_changed(self, spin_button, option):
"""Action triggered on spin button 'changed' signal."""
value = spin_button.get_value_as_int()
self.option_changed(spin_button, option, value)
# File chooser
def generate_file_chooser(self, option, path=None):
"""Generate a file chooser button to select a file."""
option_name = option["option"]
label = Label(option["label"])
default_path = option.get("default_path") or (self.runner.default_path if self.runner else "")
file_chooser = FileChooserEntry(
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=path,
default_path=default_path
)
# file_chooser.set_size_request(200, 30)
if "default_path" in option:
default_path = self.lutris_config.system_config.get(option["default_path"])
if default_path and os.path.exists(default_path):
file_chooser.entry.set_text(default_path)
if path:
# If path is relative, complete with game dir
if not os.path.isabs(path):
path = os.path.expanduser(path)
if not os.path.isabs(path):
if self.game and self.game.directory:
path = os.path.join(self.game.directory, path)
file_chooser.entry.set_text(path)
file_chooser.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(file_chooser, True, True, 0)
self.option_widget = file_chooser
file_chooser.entry.connect("changed", self._on_chooser_file_set, option_name)
def _on_chooser_file_set(self, entry, option):
"""Action triggered on file select dialog 'file-set' signal."""
if not os.path.isabs(entry.get_text()):
entry.set_text(os.path.expanduser(entry.get_text()))
self.option_changed(entry.get_parent(), option, entry.get_text())
# Directory chooser
def generate_directory_chooser(self, option, path=None):
"""Generate a file chooser button to select a directory."""
label = Label(option["label"])
option_name = option["option"]
default_path = None
if not path and self.game and self.game.runner:
default_path = self.game.runner.working_dir
directory_chooser = FileChooserEntry(
title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
)
directory_chooser.entry.connect("changed", self._on_chooser_dir_set, option_name)
directory_chooser.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(directory_chooser, True, True, 0)
self.option_widget = directory_chooser
def _on_chooser_dir_set(self, entry, option):
"""Action triggered on file select dialog 'file-set' signal."""
self.option_changed(entry.get_parent(), option, entry.get_text())
# Editable grid
def generate_editable_grid(self, option_name, label, value=None):
"""Adds an editable grid widget"""
value = value or {}
try:
value = list(value.items())
except AttributeError:
logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value)
value = {}
label = Label(label)
grid = EditableGrid(value, columns=["Key", "Value"])
grid.connect("changed", self._on_grid_changed, option_name)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(grid, True, True, 0)
self.option_widget = grid
return grid
def _on_grid_changed(self, grid, option):
values = dict(grid.get_data())
self.option_changed(grid, option, values)
# Multiple file selector
def generate_multiple_file_chooser(self, option_name, label, value=None):
"""Generate a multiple file selector."""
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
label = Label(label + ":")
label.set_halign(Gtk.Align.START)
button = Gtk.Button(_("Add files"))
button.connect("clicked", self.on_add_files_clicked, option_name, value)
button.set_margin_left(10)
vbox.pack_start(label, False, False, 5)
vbox.pack_end(button, False, False, 0)
if value:
if isinstance(value, str):
self.files = [value]
else:
self.files = value
else:
self.files = []
self.files_list_store = Gtk.ListStore(str)
for filename in self.files:
self.files_list_store.append([filename])
cell_renderer = Gtk.CellRendererText()
files_treeview = Gtk.TreeView(self.files_list_store)
files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
files_treeview.append_column(files_column)
files_treeview.connect("key-press-event", self.on_files_treeview_keypress, option_name)
treeview_scroll = Gtk.ScrolledWindow()
treeview_scroll.set_min_content_height(130)
treeview_scroll.set_margin_left(10)
treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
treeview_scroll.add(files_treeview)
vbox.pack_start(treeview_scroll, True, True, 0)
self.wrapper.pack_start(vbox, True, True, 0)
self.option_widget = self.files_list_store
def on_add_files_clicked(self, _widget, option_name, value):
"""Create and run multi-file chooser dialog."""
dialog = Gtk.FileChooserNative.new(
_("Select files"),
None,
Gtk.FileChooserAction.OPEN,
_("_Add"),
_("_Cancel"),
)
dialog.set_select_multiple(True)
first_file_dir = os.path.dirname(value[0]) if value else None
dialog.set_current_folder(
first_file_dir or self.game.directory or self.config.get("game_path") or os.path.expanduser("~")
)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
self.add_files_to_treeview(dialog, option_name, self.wrapper)
dialog.destroy()
def add_files_to_treeview(self, dialog, option, wrapper):
"""Add several files to the configuration"""
filenames = dialog.get_filenames()
files = self.config.get(option, [])
for filename in filenames:
self.files_list_store.append([filename])
if filename not in files:
files.append(filename)
self.option_changed(wrapper, option, files)
def on_files_treeview_keypress(self, treeview, event, option):
"""Action triggered when a row is deleted from the filechooser."""
key = event.keyval
if key == Gdk.KEY_Delete:
selection = treeview.get_selection()
(model, treepaths) = selection.get_selected_rows()
for treepath in treepaths:
row_index = int(str(treepath))
treeiter = model.get_iter(treepath)
model.remove(treeiter)
self.raw_config[option].pop(row_index)
@staticmethod
def on_query_tooltip(_widget, x, y, keybmode, tooltip, text): # pylint: disable=unused-argument
"""Prepare a custom tooltip with a fixed width"""
label = Label(text)
label.set_use_markup(True)
label.set_max_width_chars(60)
hbox = Gtk.Box()
hbox.pack_start(label, False, False, 0)
hbox.show_all()
tooltip.set_custom(hbox)
return True
def option_changed(self, widget, option_name, value):
"""Common actions when value changed on a widget"""
self.raw_config[option_name] = value
self.config[option_name] = value
wrapper = widget.get_parent()
hbox = wrapper.get_parent()
# Dirty way to get the reset btn. I tried passing it through the
# methods but got some strange unreliable behavior.
reset_btn = hbox.get_children()[1].get_children()[0]
reset_btn.set_visible(True)
self.set_style_property("font-weight", "bold", wrapper)
def on_reset_button_clicked(self, btn, option, _widget, wrapper):
"""Clear option (remove from config, reset option widget)."""
option_key = option["option"]
current_value = self.config[option_key]
btn.set_visible(False)
self.set_style_property("font-weight", "normal", wrapper)
self.raw_config.pop(option_key)
self.lutris_config.update_cascaded_config()
reset_value = self.config.get(option_key)
if current_value == reset_value:
return
# Destroy and recreate option widget
self.wrapper = wrapper
children = wrapper.get_children()
for child in children:
child.destroy()
self.call_widget_generator(option, option_key, reset_value, option.get("default"))
self.wrapper.show_all()
@staticmethod
def set_style_property(property_, value, wrapper):
"""Add custom style."""
style_provider = Gtk.CssProvider()
style_provider.load_from_data("GtkHBox {{{}: {};}}".format(property_, value).encode())
style_context = wrapper.get_style_context()
style_context.add_provider(style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
__init__(self, game=None)
special
¶
Source code in lutris/gui/config/boxes.py
def __init__(self, game=None):
super().__init__()
self.options = []
self.game = game
self.config = None
self.raw_config = None
self.option_widget = None
self.wrapper = None
self.tooltip_default = None
self.files = []
self.files_list_store = None
add_files_to_treeview(self, dialog, option, wrapper)
¶
Add several files to the configuration
Source code in lutris/gui/config/boxes.py
def add_files_to_treeview(self, dialog, option, wrapper):
"""Add several files to the configuration"""
filenames = dialog.get_filenames()
files = self.config.get(option, [])
for filename in filenames:
self.files_list_store.append([filename])
if filename not in files:
files.append(filename)
self.option_changed(wrapper, option, files)
call_widget_generator(self, option, option_key, value, default)
¶
Call the right generation method depending on option type.
Source code in lutris/gui/config/boxes.py
def call_widget_generator(self, option, option_key, value, default): # noqa: C901
"""Call the right generation method depending on option type."""
# pylint: disable=too-many-branches
option_type = option["type"]
option_size = option.get("size", None)
if option_key in self.raw_config:
self.set_style_property("font-weight", "bold", self.wrapper)
elif value != default:
self.set_style_property("font-style", "italic", self.wrapper)
if option_type == "choice":
self.generate_combobox(option_key, option["choices"], option["label"], value, default)
elif option_type == "choice_with_entry":
self.generate_combobox(
option_key,
option["choices"],
option["label"],
value,
default,
has_entry=True,
)
elif option_type == "choice_with_search":
self.generate_searchable_combobox(
option_key,
option["choices"],
option["label"],
value,
default,
)
elif option_type == "bool":
self.generate_checkbox(option, value)
self.tooltip_default = "Enabled" if default else "Disabled"
elif option_type == "extended_bool":
self.generate_checkbox_with_callback(option, value)
self.tooltip_default = "Enabled" if default else "Disabled"
elif option_type == "range":
self.generate_range(option_key, option["min"], option["max"], option["label"], value)
elif option_type == "string":
if "label" not in option:
raise ValueError("Option %s has no label" % option)
self.generate_entry(option_key, option["label"], value, option_size)
elif option_type == "directory_chooser":
self.generate_directory_chooser(option, value)
elif option_type == "file":
self.generate_file_chooser(option, value)
elif option_type == "multiple":
self.generate_multiple_file_chooser(option_key, option["label"], value)
elif option_type == "label":
self.generate_label(option["label"])
elif option_type == "mapping":
self.generate_editable_grid(option_key, label=option["label"], value=value)
else:
raise ValueError("Unknown widget type %s" % option_type)
checkbox_toggle(self, widget, _gparam, option_name)
¶
Action for the checkbox's toggled signal.
Source code in lutris/gui/config/boxes.py
def checkbox_toggle(self, widget, _gparam, option_name):
"""Action for the checkbox's toggled signal."""
self.option_changed(widget, option_name, widget.get_active())
entry_changed(self, entry, option_name)
¶
Action triggered for entry 'changed' signal.
Source code in lutris/gui/config/boxes.py
def entry_changed(self, entry, option_name):
"""Action triggered for entry 'changed' signal."""
self.option_changed(entry, option_name, entry.get_text())
generate_checkbox(self, option, value=None)
¶
Generate a checkbox.
Source code in lutris/gui/config/boxes.py
def generate_checkbox(self, option, value=None):
"""Generate a checkbox."""
label = Label(option["label"])
self.wrapper.pack_start(label, False, False, 0)
switch = Gtk.Switch()
if value is True:
switch.set_active(value)
switch.connect("notify::active", self.checkbox_toggle, option["option"])
switch.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(switch, False, False, 0)
self.option_widget = switch
generate_checkbox_with_callback(self, option, value=None)
¶
Generate a checkbox. With callback
Source code in lutris/gui/config/boxes.py
def generate_checkbox_with_callback(self, option, value=None):
"""Generate a checkbox. With callback"""
label = Label(option["label"])
self.wrapper.pack_start(label, False, False, 0)
checkbox = Gtk.Switch()
checkbox.set_sensitive(option["active"] is True)
if value is True:
checkbox.set_active(value)
checkbox.connect("notify::active", self._on_toggle_with_callback, option)
checkbox.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(checkbox, False, False, 0)
self.option_widget = checkbox
generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False)
¶
Generate a combobox (drop-down menu).
Source code in lutris/gui/config/boxes.py
def generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False):
"""Generate a combobox (drop-down menu)."""
liststore = Gtk.ListStore(str, str)
self._populate_combobox_choices(liststore, choices, default)
# With entry ("choice_with_entry" type)
if has_entry:
combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
combobox.set_entry_text_column(0)
if value:
combobox.get_child().set_text(value)
# No entry ("choice" type)
else:
combobox = Gtk.ComboBox.new_with_model(liststore)
cell = Gtk.CellRendererText()
combobox.pack_start(cell, True)
combobox.add_attribute(cell, "text", 0)
combobox.set_id_column(1)
choices = list(v for k, v in choices)
if value in choices:
combobox.set_active_id(value)
else:
combobox.set_active_id(default)
combobox.connect("changed", self.on_combobox_change, option_name)
combobox.connect("scroll-event", self._on_combobox_scroll)
label = Label(label)
combobox.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(combobox, True, True, 0)
self.option_widget = combobox
generate_directory_chooser(self, option, path=None)
¶
Generate a file chooser button to select a directory.
Source code in lutris/gui/config/boxes.py
def generate_directory_chooser(self, option, path=None):
"""Generate a file chooser button to select a directory."""
label = Label(option["label"])
option_name = option["option"]
default_path = None
if not path and self.game and self.game.runner:
default_path = self.game.runner.working_dir
directory_chooser = FileChooserEntry(
title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
)
directory_chooser.entry.connect("changed", self._on_chooser_dir_set, option_name)
directory_chooser.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(directory_chooser, True, True, 0)
self.option_widget = directory_chooser
generate_editable_grid(self, option_name, label, value=None)
¶
Adds an editable grid widget
Source code in lutris/gui/config/boxes.py
def generate_editable_grid(self, option_name, label, value=None):
"""Adds an editable grid widget"""
value = value or {}
try:
value = list(value.items())
except AttributeError:
logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value)
value = {}
label = Label(label)
grid = EditableGrid(value, columns=["Key", "Value"])
grid.connect("changed", self._on_grid_changed, option_name)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(grid, True, True, 0)
self.option_widget = grid
return grid
generate_entry(self, option_name, label, value=None, option_size=None)
¶
Generate an entry box.
Source code in lutris/gui/config/boxes.py
def generate_entry(self, option_name, label, value=None, option_size=None):
"""Generate an entry box."""
label = Label(label)
self.wrapper.pack_start(label, False, False, 0)
entry = Gtk.Entry()
if value:
entry.set_text(value)
entry.connect("changed", self.entry_changed, option_name)
expand = option_size != "small"
self.wrapper.pack_start(entry, expand, expand, 0)
self.option_widget = entry
generate_file_chooser(self, option, path=None)
¶
Generate a file chooser button to select a file.
Source code in lutris/gui/config/boxes.py
def generate_file_chooser(self, option, path=None):
"""Generate a file chooser button to select a file."""
option_name = option["option"]
label = Label(option["label"])
default_path = option.get("default_path") or (self.runner.default_path if self.runner else "")
file_chooser = FileChooserEntry(
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=path,
default_path=default_path
)
# file_chooser.set_size_request(200, 30)
if "default_path" in option:
default_path = self.lutris_config.system_config.get(option["default_path"])
if default_path and os.path.exists(default_path):
file_chooser.entry.set_text(default_path)
if path:
# If path is relative, complete with game dir
if not os.path.isabs(path):
path = os.path.expanduser(path)
if not os.path.isabs(path):
if self.game and self.game.directory:
path = os.path.join(self.game.directory, path)
file_chooser.entry.set_text(path)
file_chooser.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(file_chooser, True, True, 0)
self.option_widget = file_chooser
file_chooser.entry.connect("changed", self._on_chooser_file_set, option_name)
generate_label(self, text)
¶
Generate a simple label.
Source code in lutris/gui/config/boxes.py
def generate_label(self, text):
"""Generate a simple label."""
label = Label(text)
label.set_use_markup(True)
label.set_halign(Gtk.Align.START)
label.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, True, True, 0)
generate_multiple_file_chooser(self, option_name, label, value=None)
¶
Generate a multiple file selector.
Source code in lutris/gui/config/boxes.py
def generate_multiple_file_chooser(self, option_name, label, value=None):
"""Generate a multiple file selector."""
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
label = Label(label + ":")
label.set_halign(Gtk.Align.START)
button = Gtk.Button(_("Add files"))
button.connect("clicked", self.on_add_files_clicked, option_name, value)
button.set_margin_left(10)
vbox.pack_start(label, False, False, 5)
vbox.pack_end(button, False, False, 0)
if value:
if isinstance(value, str):
self.files = [value]
else:
self.files = value
else:
self.files = []
self.files_list_store = Gtk.ListStore(str)
for filename in self.files:
self.files_list_store.append([filename])
cell_renderer = Gtk.CellRendererText()
files_treeview = Gtk.TreeView(self.files_list_store)
files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
files_treeview.append_column(files_column)
files_treeview.connect("key-press-event", self.on_files_treeview_keypress, option_name)
treeview_scroll = Gtk.ScrolledWindow()
treeview_scroll.set_min_content_height(130)
treeview_scroll.set_margin_left(10)
treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
treeview_scroll.add(files_treeview)
vbox.pack_start(treeview_scroll, True, True, 0)
self.wrapper.pack_start(vbox, True, True, 0)
self.option_widget = self.files_list_store
generate_range(self, option_name, min_val, max_val, label, value=None)
¶
Generate a ranged spin button.
Source code in lutris/gui/config/boxes.py
def generate_range(self, option_name, min_val, max_val, label, value=None):
"""Generate a ranged spin button."""
adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0)
spin_button = Gtk.SpinButton()
spin_button.set_adjustment(adjustment)
if value:
spin_button.set_value(value)
spin_button.connect("changed", self.on_spin_button_changed, option_name)
label = Label(label)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(spin_button, True, True, 0)
self.option_widget = spin_button
generate_searchable_combobox(self, option_name, choice_func, label, value, default)
¶
Generate a searchable combo box
Source code in lutris/gui/config/boxes.py
def generate_searchable_combobox(self, option_name, choice_func, label, value, default):
"""Generate a searchable combo box"""
combobox = SearchableCombobox(choice_func, value or default)
combobox.connect("changed", self.on_searchable_entry_changed, option_name)
self.wrapper.pack_start(Label(label), False, False, 0)
self.wrapper.pack_start(combobox, True, True, 0)
self.option_widget = combobox
generate_top_info_box(self, text)
¶
Add a top section with general help text for the current tab
Source code in lutris/gui/config/boxes.py
def generate_top_info_box(self, text):
"""Add a top section with general help text for the current tab"""
help_box = Gtk.Box()
help_box.set_margin_left(15)
help_box.set_margin_right(15)
help_box.set_margin_bottom(5)
icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU)
help_box.pack_start(icon, False, False, 5)
title_label = Gtk.Label("<i>%s</i>" % text)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_use_markup(True)
help_box.pack_start(title_label, False, False, 5)
self.pack_start(help_box, False, False, 0)
self.pack_start(Gtk.HSeparator(), False, False, 12)
help_box.show_all()
generate_widgets(self, config_section)
¶
Parse the config dict and generates widget accordingly.
Source code in lutris/gui/config/boxes.py
def generate_widgets(self, config_section): # noqa: C901 # pylint: disable=too-many-branches,too-many-statements
"""Parse the config dict and generates widget accordingly."""
if not self.options:
no_options_label = Label(_("No options available"))
no_options_label.set_halign(Gtk.Align.CENTER)
no_options_label.set_valign(Gtk.Align.CENTER)
self.pack_start(no_options_label, True, True, 0)
return
# Select config section.
if config_section == "game":
self.config = self.lutris_config.game_config
self.raw_config = self.lutris_config.raw_game_config
elif config_section == "runner":
self.config = self.lutris_config.runner_config
self.raw_config = self.lutris_config.raw_runner_config
elif config_section == "system":
self.config = self.lutris_config.system_config
self.raw_config = self.lutris_config.raw_system_config
# Go thru all options.
for option in self.options:
if "scope" in option:
if config_section not in option["scope"]:
continue
option_key = option["option"]
value = self.config.get(option_key)
default = option.get("default")
if callable(option.get("choices")) and option["type"] != "choice_with_search":
option["choices"] = option["choices"]()
if callable(option.get("condition")):
option["condition"] = option["condition"]()
self.wrapper = Gtk.Box()
self.wrapper.set_spacing(12)
self.wrapper.set_margin_bottom(6)
# Set tooltip's "Default" part
default = option.get("default")
self.tooltip_default = default if isinstance(default, str) else None
# Generate option widget
self.option_widget = None
self.call_widget_generator(option, option_key, value, default)
# Reset button
reset_btn = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
reset_btn.set_relief(Gtk.ReliefStyle.NONE)
reset_btn.set_tooltip_text(_("Reset option to global or default config"))
reset_btn.connect(
"clicked",
self.on_reset_button_clicked,
option,
self.option_widget,
self.wrapper,
)
placeholder = Gtk.Box()
placeholder.set_size_request(32, 32)
if option_key not in self.raw_config:
reset_btn.set_visible(False)
reset_btn.set_no_show_all(True)
placeholder.pack_start(reset_btn, False, False, 0)
# Tooltip
helptext = option.get("help")
if isinstance(self.tooltip_default, str):
helptext = helptext + "\n\n" if helptext else ""
helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
if value != default and option_key not in self.raw_config:
helptext = helptext + "\n\n" if helptext else ""
helptext += _(
"<i>(Italic indicates that this option is "
"modified in a lower configuration level.)</i>"
)
if helptext:
self.wrapper.props.has_tooltip = True
self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)
hbox = Gtk.Box()
hbox.set_margin_left(18)
hbox.pack_end(placeholder, False, False, 5)
# Grey out option if condition unmet
if "condition" in option and not option["condition"]:
hbox.set_sensitive(False)
# Hide if advanced
if option.get("advanced"):
hbox.get_style_context().add_class("advanced")
show_advanced = settings.read_setting("show_advanced_options")
if show_advanced != "True":
hbox.set_no_show_all(True)
hbox.pack_start(self.wrapper, True, True, 0)
self.pack_start(hbox, False, False, 0)
on_add_files_clicked(self, _widget, option_name, value)
¶
Create and run multi-file chooser dialog.
Source code in lutris/gui/config/boxes.py
def on_add_files_clicked(self, _widget, option_name, value):
"""Create and run multi-file chooser dialog."""
dialog = Gtk.FileChooserNative.new(
_("Select files"),
None,
Gtk.FileChooserAction.OPEN,
_("_Add"),
_("_Cancel"),
)
dialog.set_select_multiple(True)
first_file_dir = os.path.dirname(value[0]) if value else None
dialog.set_current_folder(
first_file_dir or self.game.directory or self.config.get("game_path") or os.path.expanduser("~")
)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
self.add_files_to_treeview(dialog, option_name, self.wrapper)
dialog.destroy()
on_combobox_change(self, combobox, option)
¶
Action triggered on combobox 'changed' signal.
Source code in lutris/gui/config/boxes.py
def on_combobox_change(self, combobox, option):
"""Action triggered on combobox 'changed' signal."""
list_store = combobox.get_model()
active = combobox.get_active()
option_value = None
if active < 0:
if combobox.get_has_entry():
option_value = combobox.get_child().get_text()
else:
option_value = list_store[active][1]
self.option_changed(combobox, option, option_value)
on_files_treeview_keypress(self, treeview, event, option)
¶
Action triggered when a row is deleted from the filechooser.
Source code in lutris/gui/config/boxes.py
def on_files_treeview_keypress(self, treeview, event, option):
"""Action triggered when a row is deleted from the filechooser."""
key = event.keyval
if key == Gdk.KEY_Delete:
selection = treeview.get_selection()
(model, treepaths) = selection.get_selected_rows()
for treepath in treepaths:
row_index = int(str(treepath))
treeiter = model.get_iter(treepath)
model.remove(treeiter)
self.raw_config[option].pop(row_index)
on_query_tooltip(_widget, x, y, keybmode, tooltip, text)
staticmethod
¶
Prepare a custom tooltip with a fixed width
Source code in lutris/gui/config/boxes.py
@staticmethod
def on_query_tooltip(_widget, x, y, keybmode, tooltip, text): # pylint: disable=unused-argument
"""Prepare a custom tooltip with a fixed width"""
label = Label(text)
label.set_use_markup(True)
label.set_max_width_chars(60)
hbox = Gtk.Box()
hbox.pack_start(label, False, False, 0)
hbox.show_all()
tooltip.set_custom(hbox)
return True
on_reset_button_clicked(self, btn, option, _widget, wrapper)
¶
Clear option (remove from config, reset option widget).
Source code in lutris/gui/config/boxes.py
def on_reset_button_clicked(self, btn, option, _widget, wrapper):
"""Clear option (remove from config, reset option widget)."""
option_key = option["option"]
current_value = self.config[option_key]
btn.set_visible(False)
self.set_style_property("font-weight", "normal", wrapper)
self.raw_config.pop(option_key)
self.lutris_config.update_cascaded_config()
reset_value = self.config.get(option_key)
if current_value == reset_value:
return
# Destroy and recreate option widget
self.wrapper = wrapper
children = wrapper.get_children()
for child in children:
child.destroy()
self.call_widget_generator(option, option_key, reset_value, option.get("default"))
self.wrapper.show_all()
on_searchable_entry_changed(self, combobox, value, key)
¶
Source code in lutris/gui/config/boxes.py
def on_searchable_entry_changed(self, combobox, value, key):
self.option_changed(combobox, key, value)
on_spin_button_changed(self, spin_button, option)
¶
Action triggered on spin button 'changed' signal.
Source code in lutris/gui/config/boxes.py
def on_spin_button_changed(self, spin_button, option):
"""Action triggered on spin button 'changed' signal."""
value = spin_button.get_value_as_int()
self.option_changed(spin_button, option, value)
option_changed(self, widget, option_name, value)
¶
Common actions when value changed on a widget
Source code in lutris/gui/config/boxes.py
def option_changed(self, widget, option_name, value):
"""Common actions when value changed on a widget"""
self.raw_config[option_name] = value
self.config[option_name] = value
wrapper = widget.get_parent()
hbox = wrapper.get_parent()
# Dirty way to get the reset btn. I tried passing it through the
# methods but got some strange unreliable behavior.
reset_btn = hbox.get_children()[1].get_children()[0]
reset_btn.set_visible(True)
self.set_style_property("font-weight", "bold", wrapper)
set_style_property(property_, value, wrapper)
staticmethod
¶
Add custom style.
Source code in lutris/gui/config/boxes.py
@staticmethod
def set_style_property(property_, value, wrapper):
"""Add custom style."""
style_provider = Gtk.CssProvider()
style_provider.load_from_data("GtkHBox {{{}: {};}}".format(property_, value).encode())
style_context = wrapper.get_style_context()
style_context.add_provider(style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
GameBox (ConfigBox)
¶
Source code in lutris/gui/config/boxes.py
class GameBox(ConfigBox):
def __init__(self, lutris_config, game):
ConfigBox.__init__(self, game)
self.lutris_config = lutris_config
if game.runner_name:
if not game.runner:
try:
self.runner = import_runner(game.runner_name)()
except InvalidRunner:
self.runner = None
else:
self.runner = game.runner
if self.runner:
self.options = self.runner.game_options
else:
logger.warning("No runner in game supplied to GameBox")
self.generate_widgets("game")
__init__(self, lutris_config, game)
special
¶
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config, game):
ConfigBox.__init__(self, game)
self.lutris_config = lutris_config
if game.runner_name:
if not game.runner:
try:
self.runner = import_runner(game.runner_name)()
except InvalidRunner:
self.runner = None
else:
self.runner = game.runner
if self.runner:
self.options = self.runner.game_options
else:
logger.warning("No runner in game supplied to GameBox")
self.generate_widgets("game")
RunnerBox (ConfigBox)
¶
Configuration box for runner specific options
Source code in lutris/gui/config/boxes.py
class RunnerBox(ConfigBox):
"""Configuration box for runner specific options"""
def __init__(self, lutris_config, game=None):
ConfigBox.__init__(self, game)
self.lutris_config = lutris_config
try:
self.runner = import_runner(self.lutris_config.runner_slug)()
except InvalidRunner:
self.runner = None
if self.runner:
self.options = self.runner.get_runner_options()
if lutris_config.level == "game":
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the base runner configuration."
))
self.generate_widgets("runner")
__init__(self, lutris_config, game=None)
special
¶
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config, game=None):
ConfigBox.__init__(self, game)
self.lutris_config = lutris_config
try:
self.runner = import_runner(self.lutris_config.runner_slug)()
except InvalidRunner:
self.runner = None
if self.runner:
self.options = self.runner.get_runner_options()
if lutris_config.level == "game":
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the base runner configuration."
))
self.generate_widgets("runner")
SystemBox (ConfigBox)
¶
Source code in lutris/gui/config/boxes.py
class SystemBox(ConfigBox):
def __init__(self, lutris_config):
ConfigBox.__init__(self)
self.lutris_config = lutris_config
self.runner = None
runner_slug = self.lutris_config.runner_slug
if runner_slug:
self.options = sysoptions.with_runner_overrides(runner_slug)
else:
self.options = sysoptions.system_options
if lutris_config.game_config_id and runner_slug:
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the base runner configuration, which themselves supersede "
"the global preferences."
))
elif runner_slug:
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the global preferences."
))
self.generate_widgets("system")
__init__(self, lutris_config)
special
¶
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config):
ConfigBox.__init__(self)
self.lutris_config = lutris_config
self.runner = None
runner_slug = self.lutris_config.runner_slug
if runner_slug:
self.options = sysoptions.with_runner_overrides(runner_slug)
else:
self.options = sysoptions.system_options
if lutris_config.game_config_id and runner_slug:
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the base runner configuration, which themselves supersede "
"the global preferences."
))
elif runner_slug:
self.generate_top_info_box(_(
"If modified, these options supersede the same options from "
"the global preferences."
))
self.generate_widgets("system")
common
¶
Shared config dialog stuff
GameDialogCommon (Dialog)
¶
Base class for config dialogs
Source code in lutris/gui/config/common.py
class GameDialogCommon(Dialog):
"""Base class for config dialogs"""
no_runner_label = _("Select a runner in the Game Info tab")
def __init__(self, title, parent=None):
super().__init__(title, parent=parent)
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT)
self.notebook = None
self.name_entry = None
self.runner_box = None
self.timer_id = None
self.game = None
self.saved = None
self.slug = None
self.slug_entry = None
self.directory_entry = None
self.year_entry = None
self.slug_change_button = None
self.runner_dropdown = None
self.banner_button = None
self.icon_button = None
self.game_box = None
self.system_box = None
self.runner_name = None
self.runner_index = None
self.lutris_config = None
# These are independent windows, but start centered over
# a parent like a dialog. Not modal, not really transient,
# and does not share modality with other windows - so it
# needs its own window group.
Gtk.WindowGroup().add_window(self)
GLib.idle_add(self.clear_transient_for)
def clear_transient_for(self):
# we need the parent set to be centered over the parent, but
# we don't want to be transient really- we want other windows
# able to come to the front.
self.set_transient_for(None)
return False
@staticmethod
def build_scrolled_window(widget):
"""Return a scrolled window containing config widgets"""
scrolled_window = Gtk.ScrolledWindow(visible=True)
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_window.add(widget)
return scrolled_window
def build_notebook(self):
self.notebook = Gtk.Notebook(visible=True)
self.notebook.set_show_border(False)
self.vbox.pack_start(self.notebook, True, True, 10)
def build_tabs(self, config_level):
"""Build tabs (for game and runner levels)"""
self.timer_id = None
if config_level == "game":
self._build_info_tab()
self._build_game_tab()
self._build_runner_tab(config_level)
self._build_system_tab(config_level)
def _build_info_tab(self):
info_box = VBox()
if self.game:
info_box.pack_start(self._get_banner_box(), False, False, 6) # Banner
info_box.pack_start(self._get_name_box(), False, False, 6) # Game name
self.runner_box = self._get_runner_box()
info_box.pack_start(self.runner_box, False, False, 6) # Runner
info_box.pack_start(self._get_year_box(), False, False, 6) # Year
if self.game:
info_box.pack_start(self._get_slug_box(), False, False, 6)
info_box.pack_start(self._get_directory_box(), False, False, 6)
info_sw = self.build_scrolled_window(info_box)
self._add_notebook_tab(info_sw, _("Game info"))
def _get_name_box(self):
box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Label(_("Name"))
box.pack_start(label, False, False, 0)
self.name_entry = Gtk.Entry()
if self.game:
self.name_entry.set_text(self.game.name)
box.pack_start(self.name_entry, True, True, 0)
return box
def _get_slug_box(self):
slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Label(_("Identifier"))
slug_box.pack_start(label, False, False, 0)
self.slug_entry = SlugEntry()
self.slug_entry.set_text(self.game.slug)
self.slug_entry.set_sensitive(False)
self.slug_entry.connect("activate", self.on_slug_entry_activate)
slug_box.pack_start(self.slug_entry, True, True, 0)
self.slug_change_button = Gtk.Button(_("Change"))
self.slug_change_button.connect("clicked", self.on_slug_change_clicked)
slug_box.pack_start(self.slug_change_button, False, False, 0)
return slug_box
def _get_directory_box(self):
"""Return widget displaying the location of the game and allowing to move it"""
box = Gtk.Box(spacing=12, margin_right=12, margin_left=12, visible=True)
label = Label(_("Directory"))
box.pack_start(label, False, False, 0)
self.directory_entry = Gtk.Entry(visible=True)
self.directory_entry.set_text(self.game.directory)
self.directory_entry.set_sensitive(False)
box.pack_start(self.directory_entry, True, True, 0)
move_button = Gtk.Button(_("Move"), visible=True)
move_button.connect("clicked", self.on_move_clicked)
box.pack_start(move_button, False, False, 0)
return box
def _get_runner_box(self):
runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
runner_label = Label(_("Runner"))
runner_box.pack_start(runner_label, False, False, 0)
self.runner_dropdown = self._get_runner_dropdown()
runner_box.pack_start(self.runner_dropdown, True, True, 0)
return runner_box
def _get_banner_box(self):
banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Label("")
banner_box.pack_start(label, False, False, 0)
self.banner_button = Gtk.Button()
self._set_image("banner")
self.banner_button.connect("clicked", self.on_custom_image_select, "banner")
banner_box.pack_start(self.banner_button, False, False, 0)
reset_banner_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
reset_banner_button.set_tooltip_text(_("Remove custom banner"))
reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner")
banner_box.pack_start(reset_banner_button, False, False, 0)
self.icon_button = Gtk.Button()
self._set_image("icon")
self.icon_button.connect("clicked", self.on_custom_image_select, "icon")
banner_box.pack_start(self.icon_button, False, False, 0)
reset_icon_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
reset_icon_button.set_tooltip_text(_("Remove custom icon"))
reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon")
banner_box.pack_start(reset_icon_button, False, False, 0)
return banner_box
def _get_year_box(self):
box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Label(_("Release year"))
box.pack_start(label, False, False, 0)
self.year_entry = NumberEntry()
if self.game:
self.year_entry.set_text(str(self.game.year or ""))
box.pack_start(self.year_entry, True, True, 0)
return box
def _set_image(self, image_format):
image = Gtk.Image()
service_media = LutrisBanner() if image_format == "banner" else LutrisIcon()
game_slug = self.game.slug if self.game else ""
image.set_from_pixbuf(service_media.get_pixbuf_for_game(game_slug))
if image_format == "banner":
self.banner_button.set_image(image)
else:
self.icon_button.set_image(image)
def _get_runner_dropdown(self):
runner_liststore = self._get_runner_liststore()
runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
runner_dropdown.set_id_column(1)
runner_index = 0
if self.runner_name:
for runner in runner_liststore:
if self.runner_name == str(runner[1]):
break
runner_index += 1
self.runner_index = runner_index
runner_dropdown.set_active(self.runner_index)
runner_dropdown.connect("changed", self.on_runner_changed)
cell = Gtk.CellRendererText()
cell.props.ellipsize = Pango.EllipsizeMode.END
runner_dropdown.pack_start(cell, True)
runner_dropdown.add_attribute(cell, "text", 0)
return runner_dropdown
@staticmethod
def _get_runner_liststore():
"""Build a ListStore with available runners."""
runner_liststore = Gtk.ListStore(str, str)
runner_liststore.append((_("Select a runner from the list"), ""))
for runner in runners.get_installed():
description = runner.description
runner_liststore.append(("%s (%s)" % (runner.human_name, description), runner.name))
return runner_liststore
def on_slug_change_clicked(self, widget):
if self.slug_entry.get_sensitive() is False:
widget.set_label(_("Apply"))
self.slug_entry.set_sensitive(True)
else:
self.change_game_slug()
def on_slug_entry_activate(self, _widget):
self.change_game_slug()
def change_game_slug(self):
self.slug = self.slug_entry.get_text()
self.slug_entry.set_sensitive(False)
self.slug_change_button.set_label(_("Change"))
def on_move_clicked(self, _button):
new_location = DirectoryDialog("Select new location for the game",
default_path=self.game.directory, parent=self)
if not new_location.folder or new_location.folder == self.game.directory:
return
move_dialog = dialogs.MoveDialog(self.game, new_location.folder)
move_dialog.connect("game-moved", self.on_game_moved)
move_dialog.move()
def on_game_moved(self, dialog):
"""Show a notification when the game is moved"""
new_directory = dialog.new_directory
if new_directory:
self.directory_entry.set_text(new_directory)
send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory))
else:
send_notification("Failed to move game", "Lutris could not move %s" % dialog.game)
def _build_game_tab(self):
if self.game and self.runner_name:
self.game.runner_name = self.runner_name
if not self.game.runner or self.game.runner.name != self.runner_name:
try:
self.game.runner = runners.import_runner(self.runner_name)()
except runners.InvalidRunner:
pass
self.game_box = GameBox(self.lutris_config, self.game)
game_sw = self.build_scrolled_window(self.game_box)
elif self.runner_name:
game = Game(None)
game.runner_name = self.runner_name
self.game_box = GameBox(self.lutris_config, game)
game_sw = self.build_scrolled_window(self.game_box)
else:
game_sw = Gtk.Label(label=self.no_runner_label)
self._add_notebook_tab(game_sw, _("Game options"))
def _build_runner_tab(self, _config_level):
if self.runner_name:
self.runner_box = RunnerBox(self.lutris_config, self.game)
runner_sw = self.build_scrolled_window(self.runner_box)
else:
runner_sw = Gtk.Label(label=self.no_runner_label)
self._add_notebook_tab(runner_sw, _("Runner options"))
def _build_system_tab(self, _config_level):
if not self.lutris_config:
raise RuntimeError("Lutris config not loaded yet")
self.system_box = SystemBox(self.lutris_config)
self._add_notebook_tab(
self.build_scrolled_window(self.system_box),
_("System options")
)
def _add_notebook_tab(self, widget, label):
self.notebook.append_page(widget, Gtk.Label(label=label))
def build_action_area(self, button_callback):
self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)
# Advanced settings checkbox
checkbox = Gtk.CheckButton(label=_("Show advanced options"))
if settings.read_setting("show_advanced_options") == "True":
checkbox.set_active(True)
checkbox.connect("toggled", self.on_show_advanced_options_toggled)
self.action_area.pack_start(checkbox, False, False, 5)
# Buttons
hbox = Gtk.Box()
cancel_button = Gtk.Button(label=_("Cancel"))
cancel_button.connect("clicked", self.on_cancel_clicked)
hbox.pack_start(cancel_button, True, True, 10)
save_button = Gtk.Button(label=_("Save"))
save_button.connect("clicked", button_callback)
hbox.pack_start(save_button, True, True, 0)
self.action_area.pack_start(hbox, True, True, 0)
def on_show_advanced_options_toggled(self, checkbox):
value = bool(checkbox.get_active())
settings.write_setting("show_advanced_options", value)
self._set_advanced_options_visible(value)
def _set_advanced_options_visible(self, value):
"""Change visibility of advanced options across all config tabs."""
widgets = self.system_box.get_children()
if self.runner_name:
widgets += self.runner_box.get_children()
if self.game:
widgets += self.game_box.get_children()
for widget in widgets:
if widget.get_style_context().has_class("advanced"):
widget.set_visible(value)
if value:
widget.set_no_show_all(not value)
widget.show_all()
def on_runner_changed(self, widget):
"""Action called when runner drop down is changed."""
new_runner_index = widget.get_active()
if self.runner_index and new_runner_index != self.runner_index:
dlg = QuestionDialog(
{
"parent": self,
"question":
_("Are you sure you want to change the runner for this game ? "
"This will reset the full configuration for this game and "
"is not reversible."),
"title":
_("Confirm runner change"),
}
)
if dlg.result == Gtk.ResponseType.YES:
self.runner_index = new_runner_index
self._switch_runner(widget)
else:
# Revert the dropdown menu to the previously selected runner
widget.set_active(self.runner_index)
else:
self.runner_index = new_runner_index
self._switch_runner(widget)
def _switch_runner(self, widget):
"""Rebuilds the UI on runner change"""
current_page = self.notebook.get_current_page()
if self.runner_index == 0:
logger.info("No runner selected, resetting configuration")
self.runner_name = None
self.lutris_config = None
else:
runner_name = widget.get_model()[self.runner_index][1]
if runner_name == self.runner_name:
logger.debug("Runner unchanged, not creating a new config")
return
logger.info("Creating new configuration with runner %s", runner_name)
self.runner_name = runner_name
self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game")
self._rebuild_tabs()
self.notebook.set_current_page(current_page)
def _rebuild_tabs(self):
for i in range(self.notebook.get_n_pages(), 1, -1):
self.notebook.remove_page(i - 1)
self._build_game_tab()
self._build_runner_tab("game")
self._build_system_tab("game")
self.show_all()
def on_cancel_clicked(self, _widget=None, _event=None):
"""Dialog destroy callback."""
if self.game:
self.game.load_config()
self.destroy()
def is_valid(self):
if not self.runner_name:
ErrorDialog(_("Runner not provided"), parent=self)
return False
if not self.name_entry.get_text():
ErrorDialog(_("Please fill in the name"), parent=self)
return False
if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"):
ErrorDialog(_("Steam AppID not provided"), parent=self)
return False
invalid_fields = []
runner_class = import_runner(self.runner_name)
runner_instance = runner_class()
for config in ["game", "runner"]:
for k, v in getattr(self.lutris_config, config + "_config").items():
option = runner_instance.find_option(config + "_options", k)
if option is None:
continue
validator = option.get("validator")
if validator is not None:
try:
res = validator(v)
logger.debug("%s validated successfully: %s", k, res)
except Exception:
invalid_fields.append(option.get("label"))
if invalid_fields:
ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self)
return False
return True
def on_save(self, _button):
"""Save game info and destroy widget. Return True if success."""
if not self.is_valid():
logger.warning(_("Current configuration is not valid, ignoring save request"))
return False
name = self.name_entry.get_text()
if not self.slug:
self.slug = slugify(name)
if not self.game:
self.game = Game()
year = None
if self.year_entry.get_text():
year = int(self.year_entry.get_text())
if not self.lutris_config.game_config_id:
self.lutris_config.game_config_id = make_game_config_id(self.slug)
runner_class = runners.import_runner(self.runner_name)
runner = runner_class(self.lutris_config)
self.game.name = name
self.game.slug = self.slug
self.game.year = year
self.game.game_config_id = self.lutris_config.game_config_id
self.game.runner = runner
self.game.runner_name = self.runner_name
self.game.is_installed = True
self.game.config = self.lutris_config
self.game.save(save_config=True)
self.destroy()
self.saved = True
return True
def on_custom_image_select(self, _widget, image_type):
dialog = Gtk.FileChooserNative.new(
_("Please choose a custom image"),
self,
Gtk.FileChooserAction.OPEN,
None,
None,
)
image_filter = Gtk.FileFilter()
image_filter.set_name(_("Images"))
image_filter.add_pixbuf_formats()
dialog.add_filter(image_filter)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
image_path = dialog.get_filename()
if image_type == "banner":
self.game.has_custom_banner = True
dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
size = BANNER_SIZE
file_format = "jpeg"
else:
self.game.has_custom_icon = True
dest_path = resources.get_icon_path(self.game.slug)
size = ICON_SIZE
file_format = "png"
pixbuf = get_pixbuf(image_path, size)
pixbuf.savev(dest_path, file_format, [], [])
self._set_image(image_type)
if image_type == "icon":
system.update_desktop_icons()
dialog.destroy()
def on_custom_image_reset_clicked(self, _widget, image_type):
if image_type == "banner":
self.game.has_custom_banner = False
dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
elif image_type == "icon":
self.game.has_custom_icon = False
dest_path = resources.get_icon_path(self.game.slug)
else:
raise ValueError("Unsupported image type %s" % image_type)
if os.path.isfile(dest_path):
os.remove(dest_path)
self._set_image(image_type)
no_runner_label
¶
__init__(self, title, parent=None)
special
¶
Source code in lutris/gui/config/common.py
def __init__(self, title, parent=None):
super().__init__(title, parent=parent)
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT)
self.notebook = None
self.name_entry = None
self.runner_box = None
self.timer_id = None
self.game = None
self.saved = None
self.slug = None
self.slug_entry = None
self.directory_entry = None
self.year_entry = None
self.slug_change_button = None
self.runner_dropdown = None
self.banner_button = None
self.icon_button = None
self.game_box = None
self.system_box = None
self.runner_name = None
self.runner_index = None
self.lutris_config = None
# These are independent windows, but start centered over
# a parent like a dialog. Not modal, not really transient,
# and does not share modality with other windows - so it
# needs its own window group.
Gtk.WindowGroup().add_window(self)
GLib.idle_add(self.clear_transient_for)
build_action_area(self, button_callback)
¶
Source code in lutris/gui/config/common.py
def build_action_area(self, button_callback):
self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)
# Advanced settings checkbox
checkbox = Gtk.CheckButton(label=_("Show advanced options"))
if settings.read_setting("show_advanced_options") == "True":
checkbox.set_active(True)
checkbox.connect("toggled", self.on_show_advanced_options_toggled)
self.action_area.pack_start(checkbox, False, False, 5)
# Buttons
hbox = Gtk.Box()
cancel_button = Gtk.Button(label=_("Cancel"))
cancel_button.connect("clicked", self.on_cancel_clicked)
hbox.pack_start(cancel_button, True, True, 10)
save_button = Gtk.Button(label=_("Save"))
save_button.connect("clicked", button_callback)
hbox.pack_start(save_button, True, True, 0)
self.action_area.pack_start(hbox, True, True, 0)
build_notebook(self)
¶
Source code in lutris/gui/config/common.py
def build_notebook(self):
self.notebook = Gtk.Notebook(visible=True)
self.notebook.set_show_border(False)
self.vbox.pack_start(self.notebook, True, True, 10)
build_scrolled_window(widget)
staticmethod
¶
Return a scrolled window containing config widgets
Source code in lutris/gui/config/common.py
@staticmethod
def build_scrolled_window(widget):
"""Return a scrolled window containing config widgets"""
scrolled_window = Gtk.ScrolledWindow(visible=True)
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_window.add(widget)
return scrolled_window
build_tabs(self, config_level)
¶
Build tabs (for game and runner levels)
Source code in lutris/gui/config/common.py
def build_tabs(self, config_level):
"""Build tabs (for game and runner levels)"""
self.timer_id = None
if config_level == "game":
self._build_info_tab()
self._build_game_tab()
self._build_runner_tab(config_level)
self._build_system_tab(config_level)
change_game_slug(self)
¶
Source code in lutris/gui/config/common.py
def change_game_slug(self):
self.slug = self.slug_entry.get_text()
self.slug_entry.set_sensitive(False)
self.slug_change_button.set_label(_("Change"))
clear_transient_for(self)
¶
Source code in lutris/gui/config/common.py
def clear_transient_for(self):
# we need the parent set to be centered over the parent, but
# we don't want to be transient really- we want other windows
# able to come to the front.
self.set_transient_for(None)
return False
is_valid(self)
¶
Source code in lutris/gui/config/common.py
def is_valid(self):
if not self.runner_name:
ErrorDialog(_("Runner not provided"), parent=self)
return False
if not self.name_entry.get_text():
ErrorDialog(_("Please fill in the name"), parent=self)
return False
if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"):
ErrorDialog(_("Steam AppID not provided"), parent=self)
return False
invalid_fields = []
runner_class = import_runner(self.runner_name)
runner_instance = runner_class()
for config in ["game", "runner"]:
for k, v in getattr(self.lutris_config, config + "_config").items():
option = runner_instance.find_option(config + "_options", k)
if option is None:
continue
validator = option.get("validator")
if validator is not None:
try:
res = validator(v)
logger.debug("%s validated successfully: %s", k, res)
except Exception:
invalid_fields.append(option.get("label"))
if invalid_fields:
ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self)
return False
return True
on_cancel_clicked(self, _widget=None, _event=None)
¶
Dialog destroy callback.
Source code in lutris/gui/config/common.py
def on_cancel_clicked(self, _widget=None, _event=None):
"""Dialog destroy callback."""
if self.game:
self.game.load_config()
self.destroy()
on_custom_image_reset_clicked(self, _widget, image_type)
¶
Source code in lutris/gui/config/common.py
def on_custom_image_reset_clicked(self, _widget, image_type):
if image_type == "banner":
self.game.has_custom_banner = False
dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
elif image_type == "icon":
self.game.has_custom_icon = False
dest_path = resources.get_icon_path(self.game.slug)
else:
raise ValueError("Unsupported image type %s" % image_type)
if os.path.isfile(dest_path):
os.remove(dest_path)
self._set_image(image_type)
on_custom_image_select(self, _widget, image_type)
¶
Source code in lutris/gui/config/common.py
def on_custom_image_select(self, _widget, image_type):
dialog = Gtk.FileChooserNative.new(
_("Please choose a custom image"),
self,
Gtk.FileChooserAction.OPEN,
None,
None,
)
image_filter = Gtk.FileFilter()
image_filter.set_name(_("Images"))
image_filter.add_pixbuf_formats()
dialog.add_filter(image_filter)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
image_path = dialog.get_filename()
if image_type == "banner":
self.game.has_custom_banner = True
dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
size = BANNER_SIZE
file_format = "jpeg"
else:
self.game.has_custom_icon = True
dest_path = resources.get_icon_path(self.game.slug)
size = ICON_SIZE
file_format = "png"
pixbuf = get_pixbuf(image_path, size)
pixbuf.savev(dest_path, file_format, [], [])
self._set_image(image_type)
if image_type == "icon":
system.update_desktop_icons()
dialog.destroy()
on_game_moved(self, dialog)
¶
Show a notification when the game is moved
Source code in lutris/gui/config/common.py
def on_game_moved(self, dialog):
"""Show a notification when the game is moved"""
new_directory = dialog.new_directory
if new_directory:
self.directory_entry.set_text(new_directory)
send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory))
else:
send_notification("Failed to move game", "Lutris could not move %s" % dialog.game)
on_move_clicked(self, _button)
¶
Source code in lutris/gui/config/common.py
def on_move_clicked(self, _button):
new_location = DirectoryDialog("Select new location for the game",
default_path=self.game.directory, parent=self)
if not new_location.folder or new_location.folder == self.game.directory:
return
move_dialog = dialogs.MoveDialog(self.game, new_location.folder)
move_dialog.connect("game-moved", self.on_game_moved)
move_dialog.move()
on_runner_changed(self, widget)
¶
Action called when runner drop down is changed.
Source code in lutris/gui/config/common.py
def on_runner_changed(self, widget):
"""Action called when runner drop down is changed."""
new_runner_index = widget.get_active()
if self.runner_index and new_runner_index != self.runner_index:
dlg = QuestionDialog(
{
"parent": self,
"question":
_("Are you sure you want to change the runner for this game ? "
"This will reset the full configuration for this game and "
"is not reversible."),
"title":
_("Confirm runner change"),
}
)
if dlg.result == Gtk.ResponseType.YES:
self.runner_index = new_runner_index
self._switch_runner(widget)
else:
# Revert the dropdown menu to the previously selected runner
widget.set_active(self.runner_index)
else:
self.runner_index = new_runner_index
self._switch_runner(widget)
on_save(self, _button)
¶
Save game info and destroy widget. Return True if success.
Source code in lutris/gui/config/common.py
def on_save(self, _button):
"""Save game info and destroy widget. Return True if success."""
if not self.is_valid():
logger.warning(_("Current configuration is not valid, ignoring save request"))
return False
name = self.name_entry.get_text()
if not self.slug:
self.slug = slugify(name)
if not self.game:
self.game = Game()
year = None
if self.year_entry.get_text():
year = int(self.year_entry.get_text())
if not self.lutris_config.game_config_id:
self.lutris_config.game_config_id = make_game_config_id(self.slug)
runner_class = runners.import_runner(self.runner_name)
runner = runner_class(self.lutris_config)
self.game.name = name
self.game.slug = self.slug
self.game.year = year
self.game.game_config_id = self.lutris_config.game_config_id
self.game.runner = runner
self.game.runner_name = self.runner_name
self.game.is_installed = True
self.game.config = self.lutris_config
self.game.save(save_config=True)
self.destroy()
self.saved = True
return True
on_show_advanced_options_toggled(self, checkbox)
¶
Source code in lutris/gui/config/common.py
def on_show_advanced_options_toggled(self, checkbox):
value = bool(checkbox.get_active())
settings.write_setting("show_advanced_options", value)
self._set_advanced_options_visible(value)
on_slug_change_clicked(self, widget)
¶
Source code in lutris/gui/config/common.py
def on_slug_change_clicked(self, widget):
if self.slug_entry.get_sensitive() is False:
widget.set_label(_("Apply"))
self.slug_entry.set_sensitive(True)
else:
self.change_game_slug()
on_slug_entry_activate(self, _widget)
¶
Source code in lutris/gui/config/common.py
def on_slug_entry_activate(self, _widget):
self.change_game_slug()
edit_game
¶
EditGameConfigDialog (GameDialogCommon)
¶
Game config edit dialog.
Source code in lutris/gui/config/edit_game.py
class EditGameConfigDialog(GameDialogCommon):
"""Game config edit dialog."""
def __init__(self, parent, game):
super().__init__(_("Configure %s") % game.name, parent=parent)
self.game = game
self.lutris_config = game.config
self.slug = game.slug
self.runner_name = game.runner_name
self.build_notebook()
self.build_tabs("game")
self.build_action_area(self.on_save)
self.connect("delete-event", self.on_cancel_clicked)
self.show_all()
__init__(self, parent, game)
special
¶
Source code in lutris/gui/config/edit_game.py
def __init__(self, parent, game):
super().__init__(_("Configure %s") % game.name, parent=parent)
self.game = game
self.lutris_config = game.config
self.slug = game.slug
self.runner_name = game.runner_name
self.build_notebook()
self.build_tabs("game")
self.build_action_area(self.on_save)
self.connect("delete-event", self.on_cancel_clicked)
self.show_all()
preferences_box
¶
PreferencesBox (VBox)
¶
Source code in lutris/gui/config/preferences_box.py
class PreferencesBox(VBox):
settings_options = {
"hide_client_on_game_start": _("Minimize client when a game is launched"),
"hide_text_under_icons": _("Hide text under icons (requires restart)"),
"show_tray_icon": _("Show Tray Icon (requires restart)"),
"dark_theme": _("Use dark theme (requires dark theme variant for Gtk)")
}
def _get_section_label(self, text):
label = Gtk.Label(visible=True)
label.set_markup("<b>%s</b>" % text)
label.set_alignment(0, 0.5)
return label
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(50)
self.set_margin_bottom(50)
self.set_margin_right(80)
self.set_margin_left(80)
self.add(self._get_section_label(_("Interface options")))
listbox = Gtk.ListBox(visible=True)
self.pack_start(listbox, False, False, 12)
for setting_key, label in self.settings_options.items():
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(self._get_setting_box(setting_key, label))
listbox.add(list_box_row)
def _get_setting_box(self, setting_key, label):
box = Gtk.Box(
spacing=12,
margin_top=12,
margin_bottom=12,
visible=True
)
label = Gtk.Label(label, visible=True)
label.set_alignment(0, 0.5)
box.pack_start(label, True, True, 12)
checkbox = Gtk.Switch(visible=True)
if settings.read_setting(setting_key).lower() == "true":
checkbox.set_active(True)
checkbox.connect("state-set", self._on_setting_change, setting_key)
box.pack_start(checkbox, False, False, 12)
return box
def _on_setting_change(self, widget, state, setting_key):
"""Save a setting when an option is toggled"""
settings.write_setting(setting_key, state)
if setting_key == "dark_theme":
application = Gio.Application.get_default()
application.style_manager.is_config_dark = state
settings_options
¶
__init__(self)
special
¶
Source code in lutris/gui/config/preferences_box.py
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(50)
self.set_margin_bottom(50)
self.set_margin_right(80)
self.set_margin_left(80)
self.add(self._get_section_label(_("Interface options")))
listbox = Gtk.ListBox(visible=True)
self.pack_start(listbox, False, False, 12)
for setting_key, label in self.settings_options.items():
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(self._get_setting_box(setting_key, label))
listbox.add(list_box_row)
preferences_dialog
¶
Configuration dialog for client and system options
PreferencesDialog (GameDialogCommon)
¶
Source code in lutris/gui/config/preferences_dialog.py
class PreferencesDialog(GameDialogCommon):
def __init__(self, parent=None):
super().__init__(_("Lutris settings"), parent=parent)
self.set_border_width(0)
self.set_default_size(1010, 600)
self.lutris_config = LutrisConfig()
hbox = Gtk.HBox(visible=True)
sidebar = Gtk.ListBox(visible=True)
sidebar.connect("row-selected", self.on_sidebar_activated)
sidebar.add(self.get_sidebar_button("prefs-stack", _("Interface"), "view-grid-symbolic"))
sidebar.add(self.get_sidebar_button("runners-stack", _("Runners"), "applications-utilities-symbolic"))
sidebar.add(self.get_sidebar_button("services-stack", _("Sources"), "application-x-addon-symbolic"))
sidebar.add(self.get_sidebar_button("sysinfo-stack", _("Hardware information"), "computer-symbolic"))
sidebar.add(self.get_sidebar_button("system-stack", _("Global options"), "emblem-system-symbolic"))
hbox.pack_start(sidebar, False, False, 0)
self.stack = Gtk.Stack(visible=True)
self.stack.set_vhomogeneous(False)
self.stack.set_interpolate_size(True)
hbox.add(self.stack)
self.vbox.pack_start(hbox, True, True, 0)
self.stack.add_named(
self.build_scrolled_window(PreferencesBox()),
"prefs-stack"
)
self.stack.add_named(
self.build_scrolled_window(RunnersBox()),
"runners-stack"
)
self.stack.add_named(
self.build_scrolled_window(ServicesBox()),
"services-stack"
)
self.stack.add_named(
self.build_scrolled_window(SysInfoBox()),
"sysinfo-stack"
)
self.system_box = SystemBox(self.lutris_config)
self.system_box.show_all()
self.stack.add_named(
self.build_scrolled_window(self.system_box),
"system-stack"
)
self.build_action_area(self.on_save)
self.action_area.set_margin_bottom(12)
self.action_area.set_margin_right(12)
self.action_area.set_margin_left(12)
self.action_area.set_margin_top(12)
def on_sidebar_activated(self, _listbox, row):
if row.get_children()[0].stack_id == "system-stack":
self.action_area.show_all()
else:
self.action_area.hide()
self.stack.set_visible_child_name(row.get_children()[0].stack_id)
def get_sidebar_button(self, stack_id, text, icon_name):
hbox = Gtk.HBox(visible=True)
hbox.stack_id = stack_id
hbox.set_margin_top(12)
hbox.set_margin_bottom(12)
hbox.set_margin_right(40)
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
icon.show()
hbox.pack_start(icon, False, False, 6)
label = Gtk.Label(text, visible=True)
label.set_alignment(0, 0.5)
hbox.pack_start(label, False, False, 6)
return hbox
def on_save(self, _widget):
self.lutris_config.save()
self.destroy()
__init__(self, parent=None)
special
¶
Source code in lutris/gui/config/preferences_dialog.py
def __init__(self, parent=None):
super().__init__(_("Lutris settings"), parent=parent)
self.set_border_width(0)
self.set_default_size(1010, 600)
self.lutris_config = LutrisConfig()
hbox = Gtk.HBox(visible=True)
sidebar = Gtk.ListBox(visible=True)
sidebar.connect("row-selected", self.on_sidebar_activated)
sidebar.add(self.get_sidebar_button("prefs-stack", _("Interface"), "view-grid-symbolic"))
sidebar.add(self.get_sidebar_button("runners-stack", _("Runners"), "applications-utilities-symbolic"))
sidebar.add(self.get_sidebar_button("services-stack", _("Sources"), "application-x-addon-symbolic"))
sidebar.add(self.get_sidebar_button("sysinfo-stack", _("Hardware information"), "computer-symbolic"))
sidebar.add(self.get_sidebar_button("system-stack", _("Global options"), "emblem-system-symbolic"))
hbox.pack_start(sidebar, False, False, 0)
self.stack = Gtk.Stack(visible=True)
self.stack.set_vhomogeneous(False)
self.stack.set_interpolate_size(True)
hbox.add(self.stack)
self.vbox.pack_start(hbox, True, True, 0)
self.stack.add_named(
self.build_scrolled_window(PreferencesBox()),
"prefs-stack"
)
self.stack.add_named(
self.build_scrolled_window(RunnersBox()),
"runners-stack"
)
self.stack.add_named(
self.build_scrolled_window(ServicesBox()),
"services-stack"
)
self.stack.add_named(
self.build_scrolled_window(SysInfoBox()),
"sysinfo-stack"
)
self.system_box = SystemBox(self.lutris_config)
self.system_box.show_all()
self.stack.add_named(
self.build_scrolled_window(self.system_box),
"system-stack"
)
self.build_action_area(self.on_save)
self.action_area.set_margin_bottom(12)
self.action_area.set_margin_right(12)
self.action_area.set_margin_left(12)
self.action_area.set_margin_top(12)
get_sidebar_button(self, stack_id, text, icon_name)
¶
Source code in lutris/gui/config/preferences_dialog.py
def get_sidebar_button(self, stack_id, text, icon_name):
hbox = Gtk.HBox(visible=True)
hbox.stack_id = stack_id
hbox.set_margin_top(12)
hbox.set_margin_bottom(12)
hbox.set_margin_right(40)
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
icon.show()
hbox.pack_start(icon, False, False, 6)
label = Gtk.Label(text, visible=True)
label.set_alignment(0, 0.5)
hbox.pack_start(label, False, False, 6)
return hbox
on_save(self, _widget)
¶
Save game info and destroy widget. Return True if success.
Source code in lutris/gui/config/preferences_dialog.py
def on_save(self, _widget):
self.lutris_config.save()
self.destroy()
on_sidebar_activated(self, _listbox, row)
¶
Source code in lutris/gui/config/preferences_dialog.py
def on_sidebar_activated(self, _listbox, row):
if row.get_children()[0].stack_id == "system-stack":
self.action_area.show_all()
else:
self.action_area.hide()
self.stack.set_visible_child_name(row.get_children()[0].stack_id)
runner
¶
RunnerConfigDialog (GameDialogCommon)
¶
Runner config edit dialog.
Source code in lutris/gui/config/runner.py
class RunnerConfigDialog(GameDialogCommon):
"""Runner config edit dialog."""
def __init__(self, runner, parent=None):
super().__init__(_("Configure %s") % runner.human_name, parent=parent)
self.runner_name = runner.__class__.__name__
self.saved = False
self.lutris_config = LutrisConfig(runner_slug=self.runner_name)
self.build_notebook()
self.build_tabs("runner")
self.build_action_area(self.on_save)
self.show_all()
def on_save(self, wigdet, data=None):
self.lutris_config.save()
self.destroy()
__init__(self, runner, parent=None)
special
¶
Source code in lutris/gui/config/runner.py
def __init__(self, runner, parent=None):
super().__init__(_("Configure %s") % runner.human_name, parent=parent)
self.runner_name = runner.__class__.__name__
self.saved = False
self.lutris_config = LutrisConfig(runner_slug=self.runner_name)
self.build_notebook()
self.build_tabs("runner")
self.build_action_area(self.on_save)
self.show_all()
on_save(self, wigdet, data=None)
¶
Save game info and destroy widget. Return True if success.
Source code in lutris/gui/config/runner.py
def on_save(self, wigdet, data=None):
self.lutris_config.save()
self.destroy()
runner_box
¶
RunnerBox (Box)
¶
Source code in lutris/gui/config/runner_box.py
class RunnerBox(Gtk.Box):
__gsignals__ = {
"runner-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
"runner-removed": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, runner_name):
super().__init__(visible=True)
self.connect("runner-installed", self.on_runner_installed)
self.connect("runner-removed", self.on_runner_removed)
self.set_margin_bottom(12)
self.set_margin_top(12)
self.set_margin_left(12)
self.set_margin_right(12)
self.runner = runners.import_runner(runner_name)()
icon = get_icon(self.runner.name, icon_format='pixbuf', size=ICON_SIZE)
if icon:
runner_icon = Gtk.Image(visible=True)
runner_icon.set_from_pixbuf(icon)
else:
runner_icon = Gtk.Image.new_from_icon_name("package-x-generic-symbolic", Gtk.IconSize.DND)
runner_icon.show()
runner_icon.set_margin_right(12)
self.pack_start(runner_icon, False, True, 6)
self.runner_label_box = Gtk.VBox(visible=True)
self.runner_label_box.set_margin_top(12)
runner_label = Gtk.Label(visible=True)
runner_label.set_alignment(0, 0.5)
runner_label.set_markup("<b>%s</b>" % self.runner.human_name)
self.runner_label_box.pack_start(runner_label, False, False, 0)
desc_label = Gtk.Label(visible=True)
desc_label.set_line_wrap(True)
desc_label.set_alignment(0, 0.5)
desc_label.set_text(self.runner.description)
self.runner_label_box.pack_start(desc_label, False, False, 0)
self.pack_start(self.runner_label_box, True, True, 0)
self.configure_button = Gtk.Button.new_from_icon_name("preferences-system-symbolic", Gtk.IconSize.BUTTON)
self.configure_button.set_margin_right(12)
self.configure_button.connect("clicked", self.on_configure_clicked)
self.pack_start(self.configure_button, False, False, 0)
if not self.runner.is_installed():
self.runner_label_box.set_sensitive(False)
self.configure_button.show()
self.action_alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
self.action_alignment.show()
self.action_alignment.add(self.get_action_button())
self.pack_start(self.action_alignment, False, False, 0)
def get_action_button(self):
"""Return a install or remove button"""
if self.runner.multiple_versions:
_button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_versions_clicked)
else:
if self.runner.is_installed():
_button = Gtk.Button.new_from_icon_name("edit-delete-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_remove_clicked)
else:
_button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_install_clicked)
_button.show()
return _button
def on_versions_clicked(self, widget):
RunnerInstallDialog(
_("Manage %s versions") % self.runner.name,
None,
self.runner.name
)
# connect a runner-installed signal from the above dialog?
def on_install_clicked(self, widget):
"""Install a runner."""
logger.debug("Install of %s requested", self.runner)
try:
self.runner.install(downloader=simple_downloader)
except (
runners.RunnerInstallationError,
runners.NonInstallableRunnerError,
) as ex:
logger.error(ex)
ErrorDialog(ex.message)
return
if self.runner.is_installed():
self.emit("runner-installed")
else:
logger.error("Runner failed to install")
def on_configure_clicked(self, widget):
window = self.get_toplevel()
application = window.get_application()
application.show_window(RunnerConfigDialog, runner=self.runner, parent=window)
def on_remove_clicked(self, widget):
dialog = QuestionDialog(
{
"title": _("Do you want to uninstall %s?") % self.runner.human_name,
"question": _("This will remove <b>%s</b> and all associated data." % self.runner.human_name)
}
)
if Gtk.ResponseType.YES == dialog.result:
self.runner.uninstall()
self.emit("runner-removed")
def on_runner_installed(self, widget):
"""Called after the runnner is installed"""
self.runner_label_box.set_sensitive(True)
self.action_alignment.get_children()[0].destroy()
self.action_alignment.add(self.get_action_button())
def on_runner_removed(self, widget):
"""Called after the runner is removed"""
self.runner_label_box.set_sensitive(False)
self.action_alignment.get_children()[0].destroy()
self.action_alignment.add(self.get_action_button())
__init__(self, runner_name)
special
¶
Source code in lutris/gui/config/runner_box.py
def __init__(self, runner_name):
super().__init__(visible=True)
self.connect("runner-installed", self.on_runner_installed)
self.connect("runner-removed", self.on_runner_removed)
self.set_margin_bottom(12)
self.set_margin_top(12)
self.set_margin_left(12)
self.set_margin_right(12)
self.runner = runners.import_runner(runner_name)()
icon = get_icon(self.runner.name, icon_format='pixbuf', size=ICON_SIZE)
if icon:
runner_icon = Gtk.Image(visible=True)
runner_icon.set_from_pixbuf(icon)
else:
runner_icon = Gtk.Image.new_from_icon_name("package-x-generic-symbolic", Gtk.IconSize.DND)
runner_icon.show()
runner_icon.set_margin_right(12)
self.pack_start(runner_icon, False, True, 6)
self.runner_label_box = Gtk.VBox(visible=True)
self.runner_label_box.set_margin_top(12)
runner_label = Gtk.Label(visible=True)
runner_label.set_alignment(0, 0.5)
runner_label.set_markup("<b>%s</b>" % self.runner.human_name)
self.runner_label_box.pack_start(runner_label, False, False, 0)
desc_label = Gtk.Label(visible=True)
desc_label.set_line_wrap(True)
desc_label.set_alignment(0, 0.5)
desc_label.set_text(self.runner.description)
self.runner_label_box.pack_start(desc_label, False, False, 0)
self.pack_start(self.runner_label_box, True, True, 0)
self.configure_button = Gtk.Button.new_from_icon_name("preferences-system-symbolic", Gtk.IconSize.BUTTON)
self.configure_button.set_margin_right(12)
self.configure_button.connect("clicked", self.on_configure_clicked)
self.pack_start(self.configure_button, False, False, 0)
if not self.runner.is_installed():
self.runner_label_box.set_sensitive(False)
self.configure_button.show()
self.action_alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
self.action_alignment.show()
self.action_alignment.add(self.get_action_button())
self.pack_start(self.action_alignment, False, False, 0)
get_action_button(self)
¶
Return a install or remove button
Source code in lutris/gui/config/runner_box.py
def get_action_button(self):
"""Return a install or remove button"""
if self.runner.multiple_versions:
_button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_versions_clicked)
else:
if self.runner.is_installed():
_button = Gtk.Button.new_from_icon_name("edit-delete-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_remove_clicked)
else:
_button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
_button.get_style_context().add_class("circular")
_button.connect("clicked", self.on_install_clicked)
_button.show()
return _button
on_configure_clicked(self, widget)
¶
Source code in lutris/gui/config/runner_box.py
def on_configure_clicked(self, widget):
window = self.get_toplevel()
application = window.get_application()
application.show_window(RunnerConfigDialog, runner=self.runner, parent=window)
on_install_clicked(self, widget)
¶
Install a runner.
Source code in lutris/gui/config/runner_box.py
def on_install_clicked(self, widget):
"""Install a runner."""
logger.debug("Install of %s requested", self.runner)
try:
self.runner.install(downloader=simple_downloader)
except (
runners.RunnerInstallationError,
runners.NonInstallableRunnerError,
) as ex:
logger.error(ex)
ErrorDialog(ex.message)
return
if self.runner.is_installed():
self.emit("runner-installed")
else:
logger.error("Runner failed to install")
on_remove_clicked(self, widget)
¶
Source code in lutris/gui/config/runner_box.py
def on_remove_clicked(self, widget):
dialog = QuestionDialog(
{
"title": _("Do you want to uninstall %s?") % self.runner.human_name,
"question": _("This will remove <b>%s</b> and all associated data." % self.runner.human_name)
}
)
if Gtk.ResponseType.YES == dialog.result:
self.runner.uninstall()
self.emit("runner-removed")
on_runner_installed(self, widget)
¶
Called after the runnner is installed
Source code in lutris/gui/config/runner_box.py
def on_runner_installed(self, widget):
"""Called after the runnner is installed"""
self.runner_label_box.set_sensitive(True)
self.action_alignment.get_children()[0].destroy()
self.action_alignment.add(self.get_action_button())
on_runner_removed(self, widget)
¶
Called after the runner is removed
Source code in lutris/gui/config/runner_box.py
def on_runner_removed(self, widget):
"""Called after the runner is removed"""
self.runner_label_box.set_sensitive(False)
self.action_alignment.get_children()[0].destroy()
self.action_alignment.add(self.get_action_button())
on_versions_clicked(self, widget)
¶
Source code in lutris/gui/config/runner_box.py
def on_versions_clicked(self, widget):
RunnerInstallDialog(
_("Manage %s versions") % self.runner.name,
None,
self.runner.name
)
# connect a runner-installed signal from the above dialog?
runners_box
¶
Add, remove and configure runners
RunnersBox (BaseConfigBox)
¶
List of all available runners
Source code in lutris/gui/config/runners_box.py
class RunnersBox(BaseConfigBox):
"""List of all available runners"""
def __init__(self):
super().__init__()
self.add(self.get_section_label(_("Add, remove or configure runners")))
self.add(self.get_description_label(
_("Runners are programs such as emulators, engines or "
"translation layers capable of running games.")
))
self.runner_listbox = Gtk.ListBox(visible=True)
self.pack_start(self.runner_listbox, False, False, 12)
GLib.idle_add(self.populate_runners)
def populate_runners(self):
for runner_name in sorted(runners.__all__):
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(RunnerBox(runner_name))
self.runner_listbox.add(list_box_row)
@staticmethod
def on_folder_clicked(_widget):
open_uri("file://" + settings.RUNNER_DIR)
__init__(self)
special
¶
Source code in lutris/gui/config/runners_box.py
def __init__(self):
super().__init__()
self.add(self.get_section_label(_("Add, remove or configure runners")))
self.add(self.get_description_label(
_("Runners are programs such as emulators, engines or "
"translation layers capable of running games.")
))
self.runner_listbox = Gtk.ListBox(visible=True)
self.pack_start(self.runner_listbox, False, False, 12)
GLib.idle_add(self.populate_runners)
on_folder_clicked(_widget)
staticmethod
¶
Source code in lutris/gui/config/runners_box.py
@staticmethod
def on_folder_clicked(_widget):
open_uri("file://" + settings.RUNNER_DIR)
populate_runners(self)
¶
Source code in lutris/gui/config/runners_box.py
def populate_runners(self):
for runner_name in sorted(runners.__all__):
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(RunnerBox(runner_name))
self.runner_listbox.add(list_box_row)
services_box
¶
ServicesBox (BaseConfigBox)
¶
Source code in lutris/gui/config/services_box.py
class ServicesBox(BaseConfigBox):
__gsignals__ = {
"services-changed": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self):
super().__init__()
self.add(self.get_section_label(_("Enable integrations with game sources")))
self.add(self.get_description_label(
_("Access your game libraries from various sources. "
"Changes require a restart to take effect.")
))
self.listbox = Gtk.ListBox(visible=True)
self.pack_start(self.listbox, False, False, 12)
GLib.idle_add(self.populate_services)
def populate_services(self):
for service_key in SERVICES:
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(self._get_service_box(service_key))
self.listbox.add(list_box_row)
def _get_service_box(self, service_key):
box = Gtk.Box(
spacing=12,
margin_right=12,
margin_left=12,
margin_top=12,
margin_bottom=12,
visible=True,
)
service = SERVICES[service_key]
pixbuf = get_icon(service.icon, icon_format="pixbuf", size=ICON_SIZE)
if pixbuf:
icon = Gtk.Image(visible=True)
icon.set_from_pixbuf(pixbuf)
else:
icon = Gtk.Image.new_from_icon_name(service.id, Gtk.IconSize.DND)
icon.show()
box.pack_start(icon, False, False, 0)
label = Gtk.Label(service.name, visible=True)
label.set_alignment(0, 0.5)
box.pack_start(label, True, True, 0)
checkbox = Gtk.Switch(visible=True)
if settings.read_setting(service_key,
section="services").lower() == "true":
checkbox.set_active(True)
checkbox.connect("state-set", self._on_service_change, service_key)
alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
alignment.show()
alignment.add(checkbox)
box.pack_start(alignment, False, False, 6)
return box
def _on_service_change(self, widget, state, setting_key):
"""Save a setting when an option is toggled"""
settings.write_setting(setting_key, state, section="services")
self.emit("services-changed")
__init__(self)
special
¶
Source code in lutris/gui/config/services_box.py
def __init__(self):
super().__init__()
self.add(self.get_section_label(_("Enable integrations with game sources")))
self.add(self.get_description_label(
_("Access your game libraries from various sources. "
"Changes require a restart to take effect.")
))
self.listbox = Gtk.ListBox(visible=True)
self.pack_start(self.listbox, False, False, 12)
GLib.idle_add(self.populate_services)
populate_services(self)
¶
Source code in lutris/gui/config/services_box.py
def populate_services(self):
for service_key in SERVICES:
list_box_row = Gtk.ListBoxRow(visible=True)
list_box_row.set_selectable(False)
list_box_row.set_activatable(False)
list_box_row.add(self._get_service_box(service_key))
self.listbox.add(list_box_row)
sysinfo_box
¶
SysInfoBox (Fixed)
¶
Source code in lutris/gui/config/sysinfo_box.py
class SysInfoBox(Gtk.Fixed):
settings_options = {
"hide_client_on_game_start": _("Minimize client when a game is launched"),
"hide_text_under_icons": _("Hide text under icons"),
"show_tray_icon": _("Show Tray Icon"),
}
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(40)
self.set_margin_right(30)
self.set_margin_left(30)
sysinfo_frame = Gtk.Frame(visible=True)
sysinfo_frame.set_size_request(550, 455)
scrolled_window = Gtk.ScrolledWindow(visible=True)
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
sysinfo_view = LogTextView(autoscroll=False)
sysinfo_view.set_cursor_visible(False)
scrolled_window.add(sysinfo_view)
sysinfo_frame.add(scrolled_window)
sysinfo_str = gather_system_info_str()
text_buffer = sysinfo_view.get_buffer()
text_buffer.set_text(sysinfo_str)
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self._clipboard_buffer = sysinfo_str
button_copy = Gtk.Button(_("Copy to clipboard"), visible=True)
button_copy.connect("clicked", self._copy_text)
sysinfo_label = Gtk.Label(visible=True)
sysinfo_label.set_markup(_("<b>System information</b>"))
self.put(sysinfo_label, 60, 0)
self.put(sysinfo_frame, 60, 24)
self.put(button_copy, 60, 486)
def _copy_text(self, widget): # pylint: disable=unused-argument
self.clipboard.set_text(self._clipboard_buffer, -1)
settings_options
¶
__init__(self)
special
¶
Source code in lutris/gui/config/sysinfo_box.py
def __init__(self):
super().__init__(visible=True)
self.set_margin_top(40)
self.set_margin_right(30)
self.set_margin_left(30)
sysinfo_frame = Gtk.Frame(visible=True)
sysinfo_frame.set_size_request(550, 455)
scrolled_window = Gtk.ScrolledWindow(visible=True)
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
sysinfo_view = LogTextView(autoscroll=False)
sysinfo_view.set_cursor_visible(False)
scrolled_window.add(sysinfo_view)
sysinfo_frame.add(scrolled_window)
sysinfo_str = gather_system_info_str()
text_buffer = sysinfo_view.get_buffer()
text_buffer.set_text(sysinfo_str)
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self._clipboard_buffer = sysinfo_str
button_copy = Gtk.Button(_("Copy to clipboard"), visible=True)
button_copy.connect("clicked", self._copy_text)
sysinfo_label = Gtk.Label(visible=True)
sysinfo_label.set_markup(_("<b>System information</b>"))
self.put(sysinfo_label, 60, 0)
self.put(sysinfo_frame, 60, 24)
self.put(button_copy, 60, 486)
dialogs
special
¶
Commonly used dialogs
AboutDialog (GtkBuilderDialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class AboutDialog(GtkBuilderDialog):
glade_file = "about-dialog.ui"
dialog_object = "about_dialog"
def initialize(self): # pylint: disable=arguments-differ
self.dialog.set_version(settings.VERSION)
ClientLoginDialog (GtkBuilderDialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class ClientLoginDialog(GtkBuilderDialog):
glade_file = "dialog-lutris-login.ui"
dialog_object = "lutris-login"
__gsignals__ = {
"connected": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
"cancel": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
}
def __init__(self, parent):
super().__init__(parent=parent)
self.parent = parent
self.username_entry = self.builder.get_object("username_entry")
self.password_entry = self.builder.get_object("password_entry")
cancel_button = self.builder.get_object("cancel_button")
cancel_button.connect("clicked", self.on_close)
connect_button = self.builder.get_object("connect_button")
connect_button.connect("clicked", self.on_connect)
def get_credentials(self):
username = self.username_entry.get_text()
password = self.password_entry.get_text()
return username, password
def on_username_entry_activate(self, widget): # pylint: disable=unused-argument
if all(self.get_credentials()):
self.on_connect(None)
else:
self.password_entry.grab_focus()
def on_password_entry_activate(self, widget): # pylint: disable=unused-argument
if all(self.get_credentials()):
self.on_connect(None)
else:
self.username_entry.grab_focus()
def on_connect(self, widget): # pylint: disable=unused-argument
username, password = self.get_credentials()
token = api.connect(username, password)
if not token:
NoticeDialog(_("Login failed"), parent=self.parent)
else:
self.emit("connected", username)
self.dialog.destroy()
dialog_object
¶
glade_file
¶
__init__(self, parent)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent):
super().__init__(parent=parent)
self.parent = parent
self.username_entry = self.builder.get_object("username_entry")
self.password_entry = self.builder.get_object("password_entry")
cancel_button = self.builder.get_object("cancel_button")
cancel_button.connect("clicked", self.on_close)
connect_button = self.builder.get_object("connect_button")
connect_button.connect("clicked", self.on_connect)
get_credentials(self)
¶
Source code in lutris/gui/dialogs/__init__.py
def get_credentials(self):
username = self.username_entry.get_text()
password = self.password_entry.get_text()
return username, password
on_connect(self, widget)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_connect(self, widget): # pylint: disable=unused-argument
username, password = self.get_credentials()
token = api.connect(username, password)
if not token:
NoticeDialog(_("Login failed"), parent=self.parent)
else:
self.emit("connected", username)
self.dialog.destroy()
on_password_entry_activate(self, widget)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_password_entry_activate(self, widget): # pylint: disable=unused-argument
if all(self.get_credentials()):
self.on_connect(None)
else:
self.username_entry.grab_focus()
on_username_entry_activate(self, widget)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_username_entry_activate(self, widget): # pylint: disable=unused-argument
if all(self.get_credentials()):
self.on_connect(None)
else:
self.password_entry.grab_focus()
Dialog (Dialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class Dialog(Gtk.Dialog):
def __init__(self, title=None, parent=None, flags=0, buttons=None):
super().__init__(title, parent, flags, buttons)
self.set_border_width(10)
self.connect("delete-event", self.on_destroy)
self.set_destroy_with_parent(True)
def on_destroy(self, _widget, _data=None):
self.destroy()
__init__(self, title=None, parent=None, flags=0, buttons=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, title=None, parent=None, flags=0, buttons=None):
super().__init__(title, parent, flags, buttons)
self.set_border_width(10)
self.connect("delete-event", self.on_destroy)
self.set_destroy_with_parent(True)
on_destroy(self, _widget, _data=None)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_destroy(self, _widget, _data=None):
self.destroy()
DirectoryDialog
¶
Ask the user to select a directory.
Source code in lutris/gui/dialogs/__init__.py
class DirectoryDialog:
"""Ask the user to select a directory."""
def __init__(self, message, default_path=None, parent=None):
self.folder = None
dialog = Gtk.FileChooserNative.new(
message,
parent,
Gtk.FileChooserAction.SELECT_FOLDER,
_("_OK"),
_("_Cancel"),
)
if default_path:
dialog.set_current_folder(default_path)
self.result = dialog.run()
if self.result == Gtk.ResponseType.ACCEPT:
self.folder = dialog.get_filename()
dialog.destroy()
__init__(self, message, default_path=None, parent=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, default_path=None, parent=None):
self.folder = None
dialog = Gtk.FileChooserNative.new(
message,
parent,
Gtk.FileChooserAction.SELECT_FOLDER,
_("_OK"),
_("_Cancel"),
)
if default_path:
dialog.set_current_folder(default_path)
self.result = dialog.run()
if self.result == Gtk.ResponseType.ACCEPT:
self.folder = dialog.get_filename()
dialog.destroy()
DontShowAgainDialog (MessageDialog)
¶
Display a message to the user and offer an option not to display this dialog again.
Source code in lutris/gui/dialogs/__init__.py
class DontShowAgainDialog(Gtk.MessageDialog):
"""Display a message to the user and offer an option not to display this dialog again."""
def __init__(
self,
setting,
message,
secondary_message=None,
parent=None,
checkbox_message=None,
):
# pylint: disable=no-member
if settings.read_setting(setting) == "True":
logger.info("Dialog %s dismissed by user", setting)
return
super().__init__(type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, parent=parent)
self.set_border_width(12)
self.set_markup("<b>%s</b>" % message)
if secondary_message:
self.props.secondary_use_markup = True
self.props.secondary_text = secondary_message
if not checkbox_message:
checkbox_message = _("Do not display this message again.")
dont_show_checkbutton = Gtk.CheckButton(checkbox_message)
dont_show_checkbutton.props.halign = Gtk.Align.CENTER
dont_show_checkbutton.show()
content_area = self.get_content_area()
content_area.pack_start(dont_show_checkbutton, False, False, 0)
self.run()
if dont_show_checkbutton.get_active():
settings.write_setting(setting, True)
self.destroy()
__init__(self, setting, message, secondary_message=None, parent=None, checkbox_message=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(
self,
setting,
message,
secondary_message=None,
parent=None,
checkbox_message=None,
):
# pylint: disable=no-member
if settings.read_setting(setting) == "True":
logger.info("Dialog %s dismissed by user", setting)
return
super().__init__(type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, parent=parent)
self.set_border_width(12)
self.set_markup("<b>%s</b>" % message)
if secondary_message:
self.props.secondary_use_markup = True
self.props.secondary_text = secondary_message
if not checkbox_message:
checkbox_message = _("Do not display this message again.")
dont_show_checkbutton = Gtk.CheckButton(checkbox_message)
dont_show_checkbutton.props.halign = Gtk.Align.CENTER
dont_show_checkbutton.show()
content_area = self.get_content_area()
content_area.pack_start(dont_show_checkbutton, False, False, 0)
self.run()
if dont_show_checkbutton.get_active():
settings.write_setting(setting, True)
self.destroy()
ErrorDialog (MessageDialog)
¶
Display an error message.
Source code in lutris/gui/dialogs/__init__.py
class ErrorDialog(Gtk.MessageDialog):
"""Display an error message."""
def __init__(self, message, secondary=None, parent=None):
super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
# Gtk doesn't wrap long labels containing no space correctly
# the length of the message is limited to avoid display issues
self.set_markup(message[:256])
if secondary:
self.format_secondary_text(secondary[:256])
self.run()
self.destroy()
__init__(self, message, secondary=None, parent=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, secondary=None, parent=None):
super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
# Gtk doesn't wrap long labels containing no space correctly
# the length of the message is limited to avoid display issues
self.set_markup(message[:256])
if secondary:
self.format_secondary_text(secondary[:256])
self.run()
self.destroy()
FileDialog
¶
Ask the user to select a file.
Source code in lutris/gui/dialogs/__init__.py
class FileDialog:
"""Ask the user to select a file."""
def __init__(self, message=None, default_path=None, mode="open"):
self.filename = None
if not message:
message = _("Please choose a file")
if mode == "save":
action = Gtk.FileChooserAction.SAVE
else:
action = Gtk.FileChooserAction.OPEN
dialog = Gtk.FileChooserNative.new(
message,
None,
action,
_("_OK"),
_("_Cancel"),
)
if default_path and os.path.exists(default_path):
dialog.set_current_folder(default_path)
dialog.set_local_only(False)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
self.filename = dialog.get_filename()
dialog.destroy()
__init__(self, message=None, default_path=None, mode='open')
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message=None, default_path=None, mode="open"):
self.filename = None
if not message:
message = _("Please choose a file")
if mode == "save":
action = Gtk.FileChooserAction.SAVE
else:
action = Gtk.FileChooserAction.OPEN
dialog = Gtk.FileChooserNative.new(
message,
None,
action,
_("_OK"),
_("_Cancel"),
)
if default_path and os.path.exists(default_path):
dialog.set_current_folder(default_path)
dialog.set_local_only(False)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
self.filename = dialog.get_filename()
dialog.destroy()
GtkBuilderDialog (Object)
¶
Source code in lutris/gui/dialogs/__init__.py
class GtkBuilderDialog(GObject.Object):
dialog_object = NotImplemented
__gsignals__ = {
"destroy": (GObject.SignalFlags.RUN_LAST, None, ()),
}
def __init__(self, parent=None, **kwargs):
# pylint: disable=no-member
super().__init__()
ui_filename = os.path.join(datapath.get(), "ui", self.glade_file)
if not os.path.exists(ui_filename):
raise ValueError("ui file does not exists: %s" % ui_filename)
self.builder = Gtk.Builder()
self.builder.add_from_file(ui_filename)
self.dialog = self.builder.get_object(self.dialog_object)
self.builder.connect_signals(self)
if parent:
self.dialog.set_transient_for(parent)
self.dialog.show_all()
self.dialog.connect("delete-event", self.on_close)
self.initialize(**kwargs)
def initialize(self, **kwargs):
"""Implement further customizations in subclasses"""
def present(self):
self.dialog.present()
def on_close(self, *args): # pylint: disable=unused-argument
"""Propagate the destroy event after closing the dialog"""
self.dialog.destroy()
self.emit("destroy")
def on_response(self, widget, response): # pylint: disable=unused-argument
if response == Gtk.ResponseType.DELETE_EVENT:
try:
self.dialog.hide()
except AttributeError:
pass
dialog_object
¶
__init__(self, parent=None, **kwargs)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent=None, **kwargs):
# pylint: disable=no-member
super().__init__()
ui_filename = os.path.join(datapath.get(), "ui", self.glade_file)
if not os.path.exists(ui_filename):
raise ValueError("ui file does not exists: %s" % ui_filename)
self.builder = Gtk.Builder()
self.builder.add_from_file(ui_filename)
self.dialog = self.builder.get_object(self.dialog_object)
self.builder.connect_signals(self)
if parent:
self.dialog.set_transient_for(parent)
self.dialog.show_all()
self.dialog.connect("delete-event", self.on_close)
self.initialize(**kwargs)
initialize(self, **kwargs)
¶
Implement further customizations in subclasses
Source code in lutris/gui/dialogs/__init__.py
def initialize(self, **kwargs):
"""Implement further customizations in subclasses"""
on_close(self, *args)
¶
Propagate the destroy event after closing the dialog
Source code in lutris/gui/dialogs/__init__.py
def on_close(self, *args): # pylint: disable=unused-argument
"""Propagate the destroy event after closing the dialog"""
self.dialog.destroy()
self.emit("destroy")
on_response(self, widget, response)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_response(self, widget, response): # pylint: disable=unused-argument
if response == Gtk.ResponseType.DELETE_EVENT:
try:
self.dialog.hide()
except AttributeError:
pass
present(self)
¶
Source code in lutris/gui/dialogs/__init__.py
def present(self):
self.dialog.present()
InstallOrPlayDialog (Dialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class InstallOrPlayDialog(Gtk.Dialog):
def __init__(self, game_name):
Gtk.Dialog.__init__(self, _("%s is already installed") % game_name)
self.connect("delete-event", lambda *x: self.destroy())
self.action = "play"
self.action_confirmed = False
self.set_size_request(320, 120)
self.set_border_width(12)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
self.get_content_area().add(vbox)
play_button = Gtk.RadioButton.new_with_label_from_widget(None, _("Launch game"))
play_button.connect("toggled", self.on_button_toggled, "play")
vbox.pack_start(play_button, False, False, 0)
install_button = Gtk.RadioButton.new_from_widget(play_button)
install_button.set_label(_("Install the game again"))
install_button.connect("toggled", self.on_button_toggled, "install")
vbox.pack_start(install_button, False, False, 0)
confirm_button = Gtk.Button(_("OK"))
confirm_button.connect("clicked", self.on_confirm)
vbox.pack_start(confirm_button, False, False, 0)
self.show_all()
self.run()
def on_button_toggled(self, button, action): # pylint: disable=unused-argument
logger.debug("Action set to %s", action)
self.action = action
def on_confirm(self, button): # pylint: disable=unused-argument
logger.debug("Action %s confirmed", self.action)
self.action_confirmed = True
self.destroy()
__init__(self, game_name)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game_name):
Gtk.Dialog.__init__(self, _("%s is already installed") % game_name)
self.connect("delete-event", lambda *x: self.destroy())
self.action = "play"
self.action_confirmed = False
self.set_size_request(320, 120)
self.set_border_width(12)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
self.get_content_area().add(vbox)
play_button = Gtk.RadioButton.new_with_label_from_widget(None, _("Launch game"))
play_button.connect("toggled", self.on_button_toggled, "play")
vbox.pack_start(play_button, False, False, 0)
install_button = Gtk.RadioButton.new_from_widget(play_button)
install_button.set_label(_("Install the game again"))
install_button.connect("toggled", self.on_button_toggled, "install")
vbox.pack_start(install_button, False, False, 0)
confirm_button = Gtk.Button(_("OK"))
confirm_button.connect("clicked", self.on_confirm)
vbox.pack_start(confirm_button, False, False, 0)
self.show_all()
self.run()
on_button_toggled(self, button, action)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_button_toggled(self, button, action): # pylint: disable=unused-argument
logger.debug("Action set to %s", action)
self.action = action
on_confirm(self, button)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_confirm(self, button): # pylint: disable=unused-argument
logger.debug("Action %s confirmed", self.action)
self.action_confirmed = True
self.destroy()
InstallerSourceDialog (Dialog)
¶
Show install script source
Source code in lutris/gui/dialogs/__init__.py
class InstallerSourceDialog(Gtk.Dialog):
"""Show install script source"""
def __init__(self, code, name, parent):
Gtk.Dialog.__init__(self, _("Install script for {}").format(name), parent=parent)
self.set_size_request(500, 350)
self.set_border_width(0)
self.scrolled_window = Gtk.ScrolledWindow()
self.scrolled_window.set_hexpand(True)
self.scrolled_window.set_vexpand(True)
source_buffer = Gtk.TextBuffer()
source_buffer.set_text(code)
source_box = LogTextView(source_buffer, autoscroll=False)
self.get_content_area().add(self.scrolled_window)
self.scrolled_window.add(source_box)
close_button = Gtk.Button(_("OK"))
close_button.connect("clicked", self.on_close)
self.get_content_area().add(close_button)
self.show_all()
def on_close(self, *args): # pylint: disable=unused-argument
self.destroy()
__init__(self, code, name, parent)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, code, name, parent):
Gtk.Dialog.__init__(self, _("Install script for {}").format(name), parent=parent)
self.set_size_request(500, 350)
self.set_border_width(0)
self.scrolled_window = Gtk.ScrolledWindow()
self.scrolled_window.set_hexpand(True)
self.scrolled_window.set_vexpand(True)
source_buffer = Gtk.TextBuffer()
source_buffer.set_text(code)
source_box = LogTextView(source_buffer, autoscroll=False)
self.get_content_area().add(self.scrolled_window)
self.scrolled_window.add(source_box)
close_button = Gtk.Button(_("OK"))
close_button.connect("clicked", self.on_close)
self.get_content_area().add(close_button)
self.show_all()
on_close(self, *args)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_close(self, *args): # pylint: disable=unused-argument
self.destroy()
LaunchConfigSelectDialog (Dialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class LaunchConfigSelectDialog(Gtk.Dialog):
def __init__(self, game, configs):
Gtk.Dialog.__init__(self, _("Select game to launch"))
self.connect("delete-event", lambda *x: self.destroy())
self.config_index = 0
self.set_size_request(320, 120)
self.set_border_width(12)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
self.get_content_area().add(vbox)
primary_game_radio = Gtk.RadioButton.new_with_label_from_widget(None, game.name)
primary_game_radio.connect("toggled", self.on_button_toggled, 0)
vbox.pack_start(primary_game_radio, False, False, 0)
for i, config in enumerate(configs):
_button = Gtk.RadioButton.new_from_widget(primary_game_radio)
_button.set_label(config["name"])
_button.connect("toggled", self.on_button_toggled, i + 1)
vbox.pack_start(_button, False, False, 0)
confirm_button = Gtk.Button(_("OK"))
confirm_button.connect("clicked", self.on_confirm)
vbox.pack_start(confirm_button, False, False, 0)
self.show_all()
self.run()
def on_button_toggled(self, _button, index):
self.config_index = index
def on_confirm(self, _button):
self.destroy()
__init__(self, game, configs)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game, configs):
Gtk.Dialog.__init__(self, _("Select game to launch"))
self.connect("delete-event", lambda *x: self.destroy())
self.config_index = 0
self.set_size_request(320, 120)
self.set_border_width(12)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
self.get_content_area().add(vbox)
primary_game_radio = Gtk.RadioButton.new_with_label_from_widget(None, game.name)
primary_game_radio.connect("toggled", self.on_button_toggled, 0)
vbox.pack_start(primary_game_radio, False, False, 0)
for i, config in enumerate(configs):
_button = Gtk.RadioButton.new_from_widget(primary_game_radio)
_button.set_label(config["name"])
_button.connect("toggled", self.on_button_toggled, i + 1)
vbox.pack_start(_button, False, False, 0)
confirm_button = Gtk.Button(_("OK"))
confirm_button.connect("clicked", self.on_confirm)
vbox.pack_start(confirm_button, False, False, 0)
self.show_all()
self.run()
on_button_toggled(self, _button, index)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_button_toggled(self, _button, index):
self.config_index = index
on_confirm(self, _button)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_confirm(self, _button):
self.destroy()
LutrisInitDialog (Dialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class LutrisInitDialog(Gtk.Dialog):
def __init__(self, init_lutris):
super().__init__()
self.set_size_request(320, 60)
self.set_border_width(24)
self.set_decorated(False)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
label = Gtk.Label(_("Checking for runtime updates, please wait…"))
vbox.add(label)
self.progress = Gtk.ProgressBar(visible=True)
self.progress.set_pulse_step(0.1)
vbox.add(self.progress)
self.get_content_area().add(vbox)
self.progress_timeout = GLib.timeout_add(125, self.show_progress)
self.show_all()
self.connect("destroy", self.on_destroy)
AsyncCall(self.initialize, self.init_cb, init_lutris)
def show_progress(self):
self.progress.pulse()
return True
def initialize(self, init_lutris, *args):
init_lutris()
def init_cb(self, _result, error):
if error:
ErrorDialog(str(error))
self.destroy()
def on_destroy(self, window):
GLib.source_remove(self.progress_timeout)
return True
__init__(self, init_lutris)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, init_lutris):
super().__init__()
self.set_size_request(320, 60)
self.set_border_width(24)
self.set_decorated(False)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
label = Gtk.Label(_("Checking for runtime updates, please wait…"))
vbox.add(label)
self.progress = Gtk.ProgressBar(visible=True)
self.progress.set_pulse_step(0.1)
vbox.add(self.progress)
self.get_content_area().add(vbox)
self.progress_timeout = GLib.timeout_add(125, self.show_progress)
self.show_all()
self.connect("destroy", self.on_destroy)
AsyncCall(self.initialize, self.init_cb, init_lutris)
init_cb(self, _result, error)
¶
Source code in lutris/gui/dialogs/__init__.py
def init_cb(self, _result, error):
if error:
ErrorDialog(str(error))
self.destroy()
initialize(self, init_lutris, *args)
¶
Source code in lutris/gui/dialogs/__init__.py
def initialize(self, init_lutris, *args):
init_lutris()
on_destroy(self, window)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_destroy(self, window):
GLib.source_remove(self.progress_timeout)
return True
show_progress(self)
¶
Source code in lutris/gui/dialogs/__init__.py
def show_progress(self):
self.progress.pulse()
return True
MoveDialog (Dialog)
¶
Source code in lutris/gui/dialogs/__init__.py
class MoveDialog(Gtk.Dialog):
__gsignals__ = {
"game-moved": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, game, destination):
super().__init__()
self.game = game
self.destination = destination
self.new_directory = None
self.set_size_request(320, 60)
self.set_border_width(24)
self.set_decorated(False)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
label = Gtk.Label(_("Moving %s to %s..." % (game, destination)))
vbox.add(label)
self.progress = Gtk.ProgressBar(visible=True)
self.progress.set_pulse_step(0.1)
vbox.add(self.progress)
self.get_content_area().add(vbox)
GLib.timeout_add(125, self.show_progress)
self.show_all()
def move(self):
AsyncCall(self._move_game, self.on_game_moved)
def show_progress(self):
self.progress.pulse()
return True
def _move_game(self):
self.new_directory = self.game.move(self.destination)
def on_game_moved(self, _result, error):
if error:
ErrorDialog(str(error))
self.emit("game-moved")
self.destroy()
__init__(self, game, destination)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game, destination):
super().__init__()
self.game = game
self.destination = destination
self.new_directory = None
self.set_size_request(320, 60)
self.set_border_width(24)
self.set_decorated(False)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
label = Gtk.Label(_("Moving %s to %s..." % (game, destination)))
vbox.add(label)
self.progress = Gtk.ProgressBar(visible=True)
self.progress.set_pulse_step(0.1)
vbox.add(self.progress)
self.get_content_area().add(vbox)
GLib.timeout_add(125, self.show_progress)
self.show_all()
move(self)
¶
move(self, x:int, y:int)
Source code in lutris/gui/dialogs/__init__.py
def move(self):
AsyncCall(self._move_game, self.on_game_moved)
on_game_moved(self, _result, error)
¶
Source code in lutris/gui/dialogs/__init__.py
def on_game_moved(self, _result, error):
if error:
ErrorDialog(str(error))
self.emit("game-moved")
self.destroy()
show_progress(self)
¶
Source code in lutris/gui/dialogs/__init__.py
def show_progress(self):
self.progress.pulse()
return True
NoticeDialog (MessageDialog)
¶
Display a message to the user.
Source code in lutris/gui/dialogs/__init__.py
class NoticeDialog(Gtk.MessageDialog):
"""Display a message to the user."""
def __init__(self, message, parent=None):
super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
self.set_markup(message)
self.run()
self.destroy()
__init__(self, message, parent=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, parent=None):
super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
self.set_markup(message)
self.run()
self.destroy()
QuestionDialog (MessageDialog)
¶
Ask the user a question.
Source code in lutris/gui/dialogs/__init__.py
class QuestionDialog(Gtk.MessageDialog):
"""Ask the user a question."""
YES = Gtk.ResponseType.YES
NO = Gtk.ResponseType.NO
def __init__(self, dialog_settings):
super().__init__(message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO)
self.set_markup(dialog_settings["question"])
self.set_title(dialog_settings["title"])
if "parent" in dialog_settings:
self.set_transient_for(dialog_settings["parent"])
if "widgets" in dialog_settings:
for widget in dialog_settings["widgets"]:
self.get_message_area().add(widget)
self.result = self.run()
self.destroy()
NO
¶
YES
¶
__init__(self, dialog_settings)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, dialog_settings):
super().__init__(message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO)
self.set_markup(dialog_settings["question"])
self.set_title(dialog_settings["title"])
if "parent" in dialog_settings:
self.set_transient_for(dialog_settings["parent"])
if "widgets" in dialog_settings:
for widget in dialog_settings["widgets"]:
self.get_message_area().add(widget)
self.result = self.run()
self.destroy()
WineNotInstalledWarning (DontShowAgainDialog)
¶
Display a warning if Wine is not detected on the system
Source code in lutris/gui/dialogs/__init__.py
class WineNotInstalledWarning(DontShowAgainDialog):
"""Display a warning if Wine is not detected on the system"""
def __init__(self, parent=None):
super().__init__(
"hide-wine-systemwide-install-warning",
_("Wine is not installed on your system."),
secondary_message=_(
"Having Wine installed on your system guarantees that "
"Wine builds from Lutris will have all required dependencies.\n\nPlease "
"follow the instructions given in the <a "
"href='https://github.com/lutris/lutris/wiki/Wine-Dependencies'>Lutris Wiki</a> to "
"install Wine."
),
parent=parent,
)
__init__(self, parent=None)
special
¶
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent=None):
super().__init__(
"hide-wine-systemwide-install-warning",
_("Wine is not installed on your system."),
secondary_message=_(
"Having Wine installed on your system guarantees that "
"Wine builds from Lutris will have all required dependencies.\n\nPlease "
"follow the instructions given in the <a "
"href='https://github.com/lutris/lutris/wiki/Wine-Dependencies'>Lutris Wiki</a> to "
"install Wine."
),
parent=parent,
)
cache
¶
CacheConfigurationDialog (Dialog)
¶
Source code in lutris/gui/dialogs/cache.py
class CacheConfigurationDialog(Gtk.Dialog):
def __init__(self):
Gtk.Dialog.__init__(self, _("Cache configuration"))
self.timer_id = None
self.set_size_request(480, 150)
self.set_border_width(12)
self.get_content_area().add(self.get_cache_config())
self.show_all()
def get_cache_config(self):
"""Return the widgets for the cache configuration"""
prefs_box = Gtk.VBox()
box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Gtk.Label(_("Cache path"))
box.pack_start(label, False, False, 0)
cache_path = get_cache_path()
path_chooser = FileChooserEntry(
title=_("Set the folder for the cache path"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=cache_path
)
path_chooser.entry.connect("changed", self._on_cache_path_set)
box.pack_start(path_chooser, True, True, 0)
prefs_box.pack_start(box, False, False, 6)
cache_help_label = Gtk.Label(visible=True)
cache_help_label.set_size_request(400, -1)
cache_help_label.set_markup(_(
"If provided, this location will be used by installers to cache "
"downloaded files locally for future re-use. \nIf left empty, the "
"installer files are discarded after the install completion."
))
prefs_box.pack_start(cache_help_label, False, False, 6)
return prefs_box
def _on_cache_path_set(self, entry):
if self.timer_id:
GLib.source_remove(self.timer_id)
self.timer_id = GLib.timeout_add(1000, self.save_cache_setting, entry.get_text())
def save_cache_setting(self, value):
save_cache_path(value)
GLib.source_remove(self.timer_id)
self.timer_id = None
return False
__init__(self)
special
¶
Source code in lutris/gui/dialogs/cache.py
def __init__(self):
Gtk.Dialog.__init__(self, _("Cache configuration"))
self.timer_id = None
self.set_size_request(480, 150)
self.set_border_width(12)
self.get_content_area().add(self.get_cache_config())
self.show_all()
get_cache_config(self)
¶
Return the widgets for the cache configuration
Source code in lutris/gui/dialogs/cache.py
def get_cache_config(self):
"""Return the widgets for the cache configuration"""
prefs_box = Gtk.VBox()
box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
label = Gtk.Label(_("Cache path"))
box.pack_start(label, False, False, 0)
cache_path = get_cache_path()
path_chooser = FileChooserEntry(
title=_("Set the folder for the cache path"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=cache_path
)
path_chooser.entry.connect("changed", self._on_cache_path_set)
box.pack_start(path_chooser, True, True, 0)
prefs_box.pack_start(box, False, False, 6)
cache_help_label = Gtk.Label(visible=True)
cache_help_label.set_size_request(400, -1)
cache_help_label.set_markup(_(
"If provided, this location will be used by installers to cache "
"downloaded files locally for future re-use. \nIf left empty, the "
"installer files are discarded after the install completion."
))
prefs_box.pack_start(cache_help_label, False, False, 6)
return prefs_box
save_cache_setting(self, value)
¶
Source code in lutris/gui/dialogs/cache.py
def save_cache_setting(self, value):
save_cache_path(value)
GLib.source_remove(self.timer_id)
self.timer_id = None
return False
download
¶
DownloadDialog (Dialog)
¶
Dialog showing a download in progress.
Source code in lutris/gui/dialogs/download.py
class DownloadDialog(Gtk.Dialog):
"""Dialog showing a download in progress."""
def __init__(self, url=None, dest=None, title=None, label=None, downloader=None):
Gtk.Dialog.__init__(self, title or _("Downloading file"))
self.set_size_request(485, 104)
self.set_border_width(12)
params = {"url": url, "dest": dest, "title": label or _("Downloading %s") % url}
self.dialog_progress_box = DownloadProgressBox(params, downloader=downloader)
self.dialog_progress_box.connect("complete", self.download_complete)
self.dialog_progress_box.connect("cancel", self.download_cancelled)
self.connect("response", self.on_response)
self.get_content_area().add(self.dialog_progress_box)
self.show_all()
self.dialog_progress_box.start()
def download_complete(self, _widget, _data):
self.response(Gtk.ResponseType.OK)
self.destroy()
def download_cancelled(self, _widget, data):
self.response(Gtk.ResponseType.CANCEL)
self.destroy()
def on_response(self, _dialog, response):
if response == Gtk.ResponseType.DELETE_EVENT:
self.dialog_progress_box.downloader.cancel()
self.destroy()
__init__(self, url=None, dest=None, title=None, label=None, downloader=None)
special
¶
Source code in lutris/gui/dialogs/download.py
def __init__(self, url=None, dest=None, title=None, label=None, downloader=None):
Gtk.Dialog.__init__(self, title or _("Downloading file"))
self.set_size_request(485, 104)
self.set_border_width(12)
params = {"url": url, "dest": dest, "title": label or _("Downloading %s") % url}
self.dialog_progress_box = DownloadProgressBox(params, downloader=downloader)
self.dialog_progress_box.connect("complete", self.download_complete)
self.dialog_progress_box.connect("cancel", self.download_cancelled)
self.connect("response", self.on_response)
self.get_content_area().add(self.dialog_progress_box)
self.show_all()
self.dialog_progress_box.start()
download_cancelled(self, _widget, data)
¶
Source code in lutris/gui/dialogs/download.py
def download_cancelled(self, _widget, data):
self.response(Gtk.ResponseType.CANCEL)
self.destroy()
download_complete(self, _widget, _data)
¶
Source code in lutris/gui/dialogs/download.py
def download_complete(self, _widget, _data):
self.response(Gtk.ResponseType.OK)
self.destroy()
on_response(self, _dialog, response)
¶
Source code in lutris/gui/dialogs/download.py
def on_response(self, _dialog, response):
if response == Gtk.ResponseType.DELETE_EVENT:
self.dialog_progress_box.downloader.cancel()
self.destroy()
simple_downloader(url, destination, callback, callback_args=None)
¶
Basic downloader with a DownloadDialog
Source code in lutris/gui/dialogs/download.py
def simple_downloader(url, destination, callback, callback_args=None):
"""Basic downloader with a DownloadDialog"""
if not callback_args:
callback_args = {}
dialog = DownloadDialog(url, destination)
dialog.run()
return callback(**callback_args)
issue
¶
GUI dialog for reporting issues
IssueReportWindow (BaseApplicationWindow)
¶
Window for collecting and sending issue reports
Source code in lutris/gui/dialogs/issue.py
class IssueReportWindow(BaseApplicationWindow):
"""Window for collecting and sending issue reports"""
def __init__(self, application):
super().__init__(application)
self.title_label = Gtk.Label(visible=True)
self.vbox.add(self.title_label)
title_label = Gtk.Label()
title_label.set_markup(_("<b>Submit an issue</b>"))
self.vbox.add(title_label)
self.vbox.add(Gtk.HSeparator())
issue_entry_label = Gtk.Label(_(
"Describe the problem you're having in the text box below. "
"This information will be sent the Lutris team along with your system information. "
"You can also save this information locally if you are offline."
))
issue_entry_label.set_max_width_chars(80)
issue_entry_label.set_property("wrap", True)
self.vbox.add(issue_entry_label)
self.textview = Gtk.TextView()
self.textview.set_pixels_above_lines(12)
self.textview.set_pixels_below_lines(12)
self.textview.set_left_margin(12)
self.textview.set_right_margin(12)
self.vbox.pack_start(self.textview, True, True, 0)
self.action_buttons = Gtk.Box(spacing=6)
action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
action_buttons_alignment.add(self.action_buttons)
self.vbox.pack_start(action_buttons_alignment, False, True, 0)
cancel_button = self.get_action_button(_("C_ancel"), handler=self.on_destroy)
self.action_buttons.add(cancel_button)
save_button = self.get_action_button(_("_Save"), handler=self.on_save)
self.action_buttons.add(save_button)
self.show_all()
def get_issue_info(self):
buffer = self.textview.get_buffer()
return {
'comment': buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
'system': gather_system_info()
}
def on_save(self, _button):
"""Signal handler for the save button"""
save_dialog = Gtk.FileChooserNative.new(
_("Select a location to save the issue"),
self,
Gtk.FileChooserAction.SELECT_FOLDER,
_("_OK"),
_("_Cancel"),
)
save_dialog.connect("response", self.on_folder_selected, save_dialog)
save_dialog.show()
def on_folder_selected(self, dialog, response, _dialog):
if response != Gtk.ResponseType.ACCEPT:
return
target_path = dialog.get_filename()
if not target_path:
return
issue_path = os.path.join(target_path, "lutris-issue-report.json")
issue_info = self.get_issue_info()
with open(issue_path, "w", encoding='utf-8') as issue_file:
json.dump(issue_info, issue_file, indent=2)
dialog.destroy()
NoticeDialog(_("Issue saved in %s") % issue_path)
self.destroy()
__init__(self, application)
special
¶
Source code in lutris/gui/dialogs/issue.py
def __init__(self, application):
super().__init__(application)
self.title_label = Gtk.Label(visible=True)
self.vbox.add(self.title_label)
title_label = Gtk.Label()
title_label.set_markup(_("<b>Submit an issue</b>"))
self.vbox.add(title_label)
self.vbox.add(Gtk.HSeparator())
issue_entry_label = Gtk.Label(_(
"Describe the problem you're having in the text box below. "
"This information will be sent the Lutris team along with your system information. "
"You can also save this information locally if you are offline."
))
issue_entry_label.set_max_width_chars(80)
issue_entry_label.set_property("wrap", True)
self.vbox.add(issue_entry_label)
self.textview = Gtk.TextView()
self.textview.set_pixels_above_lines(12)
self.textview.set_pixels_below_lines(12)
self.textview.set_left_margin(12)
self.textview.set_right_margin(12)
self.vbox.pack_start(self.textview, True, True, 0)
self.action_buttons = Gtk.Box(spacing=6)
action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
action_buttons_alignment.add(self.action_buttons)
self.vbox.pack_start(action_buttons_alignment, False, True, 0)
cancel_button = self.get_action_button(_("C_ancel"), handler=self.on_destroy)
self.action_buttons.add(cancel_button)
save_button = self.get_action_button(_("_Save"), handler=self.on_save)
self.action_buttons.add(save_button)
self.show_all()
get_issue_info(self)
¶
Source code in lutris/gui/dialogs/issue.py
def get_issue_info(self):
buffer = self.textview.get_buffer()
return {
'comment': buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
'system': gather_system_info()
}
on_folder_selected(self, dialog, response, _dialog)
¶
Source code in lutris/gui/dialogs/issue.py
def on_folder_selected(self, dialog, response, _dialog):
if response != Gtk.ResponseType.ACCEPT:
return
target_path = dialog.get_filename()
if not target_path:
return
issue_path = os.path.join(target_path, "lutris-issue-report.json")
issue_info = self.get_issue_info()
with open(issue_path, "w", encoding='utf-8') as issue_file:
json.dump(issue_info, issue_file, indent=2)
dialog.destroy()
NoticeDialog(_("Issue saved in %s") % issue_path)
self.destroy()
on_save(self, _button)
¶
Signal handler for the save button
Source code in lutris/gui/dialogs/issue.py
def on_save(self, _button):
"""Signal handler for the save button"""
save_dialog = Gtk.FileChooserNative.new(
_("Select a location to save the issue"),
self,
Gtk.FileChooserAction.SELECT_FOLDER,
_("_OK"),
_("_Cancel"),
)
save_dialog.connect("response", self.on_folder_selected, save_dialog)
save_dialog.show()
log
¶
Window to show game logs
LogWindow (Object)
¶
Source code in lutris/gui/dialogs/log.py
class LogWindow(GObject.Object):
def __init__(self, title=None, buffer=None, application=None):
super().__init__()
ui_filename = os.path.join(datapath.get(), "ui/log-window.ui")
builder = Gtk.Builder()
builder.add_from_file(ui_filename)
builder.connect_signals(self)
window = builder.get_object("log_window")
window.set_title(title)
self.title = title
self.buffer = buffer
self.logtextview = LogTextView(self.buffer)
scrolled_window = builder.get_object("scrolled_window")
scrolled_window.add(self.logtextview)
self.search_entry = builder.get_object("search_entry")
self.search_entry.connect("search-changed", self.logtextview.find_first)
self.search_entry.connect("next-match", self.logtextview.find_next)
self.search_entry.connect("previous-match", self.logtextview.find_previous)
save_button = builder.get_object("save_button")
save_button.connect("clicked", self.on_save_clicked)
window.connect("key-press-event", self.on_key_press_event)
window.show_all()
def on_key_press_event(self, widget, event):
shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
if event.keyval == Gdk.KEY_Return:
if shift:
self.search_entry.emit("previous-match")
else:
self.search_entry.emit("next-match")
def on_save_clicked(self, _button):
"""Handler to save log to a file"""
now = datetime.now()
log_filename = "%s (%s).log" % (self.title, now.strftime("%Y-%m-%d-%H-%M"))
file_dialog = FileDialog(
message="Save the logs to...",
default_path=os.path.expanduser("~/%s" % log_filename),
mode="save"
)
log_path = file_dialog.filename
if not log_path:
return
text = self.buffer.get_text(
self.buffer.get_start_iter(),
self.buffer.get_end_iter(),
True
)
with open(log_path, "w", encoding='utf-8') as log_file:
log_file.write(text)
__init__(self, title=None, buffer=None, application=None)
special
¶
Source code in lutris/gui/dialogs/log.py
def __init__(self, title=None, buffer=None, application=None):
super().__init__()
ui_filename = os.path.join(datapath.get(), "ui/log-window.ui")
builder = Gtk.Builder()
builder.add_from_file(ui_filename)
builder.connect_signals(self)
window = builder.get_object("log_window")
window.set_title(title)
self.title = title
self.buffer = buffer
self.logtextview = LogTextView(self.buffer)
scrolled_window = builder.get_object("scrolled_window")
scrolled_window.add(self.logtextview)
self.search_entry = builder.get_object("search_entry")
self.search_entry.connect("search-changed", self.logtextview.find_first)
self.search_entry.connect("next-match", self.logtextview.find_next)
self.search_entry.connect("previous-match", self.logtextview.find_previous)
save_button = builder.get_object("save_button")
save_button.connect("clicked", self.on_save_clicked)
window.connect("key-press-event", self.on_key_press_event)
window.show_all()
on_key_press_event(self, widget, event)
¶
Source code in lutris/gui/dialogs/log.py
def on_key_press_event(self, widget, event):
shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
if event.keyval == Gdk.KEY_Return:
if shift:
self.search_entry.emit("previous-match")
else:
self.search_entry.emit("next-match")
on_save_clicked(self, _button)
¶
Handler to save log to a file
Source code in lutris/gui/dialogs/log.py
def on_save_clicked(self, _button):
"""Handler to save log to a file"""
now = datetime.now()
log_filename = "%s (%s).log" % (self.title, now.strftime("%Y-%m-%d-%H-%M"))
file_dialog = FileDialog(
message="Save the logs to...",
default_path=os.path.expanduser("~/%s" % log_filename),
mode="save"
)
log_path = file_dialog.filename
if not log_path:
return
text = self.buffer.get_text(
self.buffer.get_start_iter(),
self.buffer.get_end_iter(),
True
)
with open(log_path, "w", encoding='utf-8') as log_file:
log_file.write(text)
runner_install
¶
Dialog used to install versions of a runner
RunnerInstallDialog (Dialog)
¶
Dialog displaying available runner version and downloads them
Source code in lutris/gui/dialogs/runner_install.py
class RunnerInstallDialog(Dialog):
"""Dialog displaying available runner version and downloads them"""
COL_VER = 0
COL_ARCH = 1
COL_URL = 2
COL_INSTALLED = 3
COL_PROGRESS = 4
COL_USAGE = 5
def __init__(self, title, parent, runner):
super().__init__(title, parent, 0)
self.add_buttons(_("_OK"), Gtk.ButtonsType.OK)
self.runner = runner
self.runner_info = {}
self.installing = {}
self.set_default_size(640, 480)
self.runners = []
self.listbox = None
label = Gtk.Label.new(_("Waiting for response from %s") % settings.SITE_URL)
self.vbox.pack_start(label, False, False, 18)
spinner = Gtk.Spinner(visible=True)
spinner.start()
self.vbox.pack_start(spinner, False, False, 18)
self.show_all()
self.runner_store = Gtk.ListStore(str, str, str, bool, int, int)
jobs.AsyncCall(api.get_runners, self.runner_fetch_cb, self.runner)
def runner_fetch_cb(self, runner_info, error):
"""Clear the box and display versions from runner_info"""
if error:
logger.error(error)
ErrorDialog(_("Unable to get runner versions: %s") % error)
return
self.runner_info = runner_info
remote_versions = {(v["version"], v["architecture"]) for v in self.runner_info["versions"]}
local_versions = self.get_installed_versions()
for local_version in local_versions - remote_versions:
self.runner_info["versions"].append({
"version": local_version[0],
"architecture": local_version[1],
"url": "",
})
if not self.runner_info:
ErrorDialog(_("Unable to get runner versions from lutris.net"))
return
for child_widget in self.vbox.get_children():
if child_widget.get_name() not in "GtkBox":
child_widget.destroy()
label = Gtk.Label.new(_("%s version management") % self.runner_info["name"])
self.vbox.add(label)
self.installing = {}
self.connect("response", self.on_destroy)
scrolled_listbox = Gtk.ScrolledWindow()
self.listbox = Gtk.ListBox()
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
scrolled_listbox.add(self.listbox)
self.vbox.pack_start(scrolled_listbox, True, True, 14)
self.populate_store()
self.show_all()
self.populate_listboxrows(self.runner_store)
def populate_listboxrows(self, store):
for runner in store:
row = Gtk.ListBoxRow()
row.runner = runner
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
row.hbox = hbox
chk_installed = Gtk.CheckButton()
chk_installed.set_sensitive(False)
chk_installed.set_active(runner[self.COL_INSTALLED])
hbox.pack_start(chk_installed, False, True, 0)
row.chk_installed = chk_installed
lbl_version = Gtk.Label(runner[self.COL_VER])
lbl_version.set_max_width_chars(20)
lbl_version.set_property("width-chars", 20)
lbl_version.set_halign(Gtk.Align.START)
hbox.pack_start(lbl_version, False, False, 5)
arch_label = Gtk.Label(runner[self.COL_ARCH])
arch_label.set_max_width_chars(8)
arch_label.set_halign(Gtk.Align.START)
hbox.pack_start(arch_label, False, True, 5)
install_progress = Gtk.ProgressBar()
install_progress.set_show_text(True)
hbox.pack_end(install_progress, True, True, 5)
row.install_progress = install_progress
if runner[self.COL_INSTALLED]:
# Check if there are apps installed, if so, show the view apps button
app_count = runner[self.COL_USAGE] or 0
if app_count > 0:
usage_button_text = gettext.ngettext(
"_View %d game",
"_View %d games",
app_count
) % app_count
usage_button = Gtk.Button.new_with_mnemonic(usage_button_text)
usage_button.connect("button_press_event", self.on_show_apps_usage, row)
hbox.pack_end(usage_button, False, True, 2)
button = Gtk.Button()
hbox.pack_end(button, False, True, 0)
hbox.reorder_child(button, 0)
row.install_uninstall_cancel_button = button
row.handler_id = None
row.add(hbox)
self.listbox.add(row)
row.show_all()
self.update_listboxrow(row)
def update_listboxrow(self, row):
row.install_progress.set_visible(False)
row.chk_installed.set_active(row.runner[self.COL_INSTALLED])
button = row.install_uninstall_cancel_button
if row.handler_id is not None:
button.disconnect(row.handler_id)
row.handler_id = None
if row.runner[self.COL_VER] in self.installing:
button.set_label(_("Cancel"))
handler_id = button.connect("button_press_event", self.on_cancel_install, row)
else:
if row.runner[self.COL_INSTALLED]:
button.set_label(_("Uninstall"))
handler_id = button.connect("button_press_event", self.on_uninstall_runner, row)
else:
button.set_label(_("Install"))
handler_id = button.connect("button_press_event", self.on_install_runner, row)
row.install_uninstall_cancel_button = button
row.handler_id = handler_id
def on_show_apps_usage(self, _widget, _button, row):
"""Return grid with games that uses this wine version"""
runner = row.runner
runner_version = "%s-%s" % (runner[self.COL_VER], runner[self.COL_ARCH])
runner_games = get_games_by_runner(self.runner)
apps = []
for db_game in runner_games:
if not db_game["installed"]:
continue
game = Game(db_game["id"])
version = game.config.runner_config["version"]
if version != runner_version:
continue
apps.append(game)
dialog = ShowAppsDialog(_("Wine version usage"), self.get_toplevel(), runner_version, apps)
dialog.run()
dialog.destroy()
def populate_store(self):
"""Return a ListStore populated with the runner versions"""
version_usage = self.get_usage_stats()
for version_info in reversed(self.runner_info["versions"]):
is_installed = os.path.exists(self.get_runner_path(version_info["version"], version_info["architecture"]))
games_using = version_usage.get("%(version)s-%(architecture)s" % version_info)
self.runner_store.append(
[
version_info["version"], version_info["architecture"], version_info["url"], is_installed, 0,
len(games_using) if games_using else 0
]
)
def get_installed_versions(self):
"""List versions available locally"""
runner_path = os.path.join(settings.RUNNER_DIR, self.runner)
if not os.path.exists(runner_path):
return set()
return {
tuple(p.rsplit("-", 1))
for p in os.listdir(runner_path)
if "-" in p
}
def get_runner_path(self, version, arch):
"""Return the local path where the runner is/will be installed"""
return os.path.join(settings.RUNNER_DIR, self.runner, "{}-{}".format(version, arch))
def get_dest_path(self, row):
"""Return temporary path where the runners should be downloaded to"""
return os.path.join(settings.CACHE_DIR, os.path.basename(row[self.COL_URL]))
def on_installed_toggled(self, _widget, path):
row = self.runner_store[path]
if row[self.COL_VER] in self.installing:
confirm_dlg = QuestionDialog(
{
"question": _("Do you want to cancel the download?"),
"title": _("Download starting"),
}
)
if confirm_dlg.result == confirm_dlg.YES:
self.cancel_install(row)
elif row[self.COL_INSTALLED]:
self.uninstall_runner(row)
else:
self.install_runner(row)
def on_cancel_install(self, widget, button, row):
self.cancel_install(row)
def cancel_install(self, row):
"""Cancel the installation of a runner version"""
runner = row.runner
self.installing[runner[self.COL_VER]].cancel()
self.uninstall_runner(row)
runner[self.COL_PROGRESS] = 0
self.installing.pop(runner[self.COL_VER])
self.update_listboxrow(row)
row.install_progress.set_visible(False)
def on_uninstall_runner(self, widget, button, row):
self.uninstall_runner(row)
def uninstall_runner(self, row):
"""Uninstall a runner version"""
runner = row.runner
version = runner[self.COL_VER]
arch = runner[self.COL_ARCH]
system.remove_folder(self.get_runner_path(version, arch))
runner[self.COL_INSTALLED] = False
if self.runner == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
self.update_listboxrow(row)
def on_install_runner(self, _widget, _button, row):
self.install_runner(row)
def install_runner(self, row):
"""Download and install a runner version"""
runner = row.runner
row.install_progress.set_fraction(0.0)
dest_path = self.get_dest_path(runner)
url = runner[self.COL_URL]
if not url:
ErrorDialog(_("Version %s is not longer available") % runner[self.COL_VER])
return
downloader = Downloader(runner[self.COL_URL], dest_path, overwrite=True)
GLib.timeout_add(100, self.get_progress, downloader, row)
self.installing[runner[self.COL_VER]] = downloader
downloader.start()
self.update_listboxrow(row)
def get_progress(self, downloader, row):
"""Update progress bar with download progress"""
runner = row.runner
if downloader.state == downloader.CANCELLED:
return False
if downloader.state == downloader.ERROR:
self.cancel_install(row)
return False
row.install_progress.show()
downloader.check_progress()
percent_downloaded = downloader.progress_percentage
if percent_downloaded >= 1:
runner[self.COL_PROGRESS] = percent_downloaded
row.install_progress.set_fraction(percent_downloaded / 100)
else:
runner[self.COL_PROGRESS] = 1
row.install_progress.pulse()
row.install_progress.set_text = _("Downloading…")
if downloader.state == downloader.COMPLETED:
runner[self.COL_PROGRESS] = 99
row.install_progress.set_text = _("Extracting…")
self.on_runner_downloaded(row)
return False
return True
def progress_pulse(self, row):
runner = row.runner
row.install_progress.pulse()
return not runner[self.COL_INSTALLED]
def get_usage_stats(self):
"""Return the usage for each version"""
runner_games = get_games_by_runner(self.runner)
version_usage = defaultdict(list)
for db_game in runner_games:
if not db_game["installed"]:
continue
game = Game(db_game["id"])
version = game.config.runner_config["version"]
version_usage[version].append(db_game["id"])
return version_usage
def on_runner_downloaded(self, row):
"""Handler called when a runner version is downloaded"""
runner = row.runner
version = runner[self.COL_VER]
architecture = runner[self.COL_ARCH]
logger.debug("Runner %s for %s has finished downloading", version, architecture)
src = self.get_dest_path(runner)
dst = self.get_runner_path(version, architecture)
GLib.timeout_add(100, self.progress_pulse, row)
jobs.AsyncCall(self.extract, self.on_extracted, src, dst, row)
@staticmethod
def extract(src, dst, row):
"""Extract a runner archive to a destination"""
extract_archive(src, dst)
return src, row
def on_extracted(self, row_info, error):
"""Called when a runner archive is extracted"""
if error or not row_info:
ErrorDialog(_("Failed to retrieve the runner archive"), parent=self)
return
src, row = row_info
runner = row.runner
os.remove(src)
runner[self.COL_PROGRESS] = 0
runner[self.COL_INSTALLED] = True
self.installing.pop(runner[self.COL_VER])
row.install_progress.set_text = ""
row.install_progress.set_fraction(0.0)
row.install_progress.hide()
self.update_listboxrow(row)
if self.runner == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
def on_destroy(self, _dialog, _data=None):
"""Override delete handler to prevent closing while downloads are active"""
if self.installing:
return True
self.destroy()
return True
COL_ARCH
¶
COL_INSTALLED
¶
COL_PROGRESS
¶
COL_URL
¶
COL_USAGE
¶
COL_VER
¶
__init__(self, title, parent, runner)
special
¶
Source code in lutris/gui/dialogs/runner_install.py
def __init__(self, title, parent, runner):
super().__init__(title, parent, 0)
self.add_buttons(_("_OK"), Gtk.ButtonsType.OK)
self.runner = runner
self.runner_info = {}
self.installing = {}
self.set_default_size(640, 480)
self.runners = []
self.listbox = None
label = Gtk.Label.new(_("Waiting for response from %s") % settings.SITE_URL)
self.vbox.pack_start(label, False, False, 18)
spinner = Gtk.Spinner(visible=True)
spinner.start()
self.vbox.pack_start(spinner, False, False, 18)
self.show_all()
self.runner_store = Gtk.ListStore(str, str, str, bool, int, int)
jobs.AsyncCall(api.get_runners, self.runner_fetch_cb, self.runner)
cancel_install(self, row)
¶
Cancel the installation of a runner version
Source code in lutris/gui/dialogs/runner_install.py
def cancel_install(self, row):
"""Cancel the installation of a runner version"""
runner = row.runner
self.installing[runner[self.COL_VER]].cancel()
self.uninstall_runner(row)
runner[self.COL_PROGRESS] = 0
self.installing.pop(runner[self.COL_VER])
self.update_listboxrow(row)
row.install_progress.set_visible(False)
extract(src, dst, row)
staticmethod
¶
Extract a runner archive to a destination
Source code in lutris/gui/dialogs/runner_install.py
@staticmethod
def extract(src, dst, row):
"""Extract a runner archive to a destination"""
extract_archive(src, dst)
return src, row
get_dest_path(self, row)
¶
Return temporary path where the runners should be downloaded to
Source code in lutris/gui/dialogs/runner_install.py
def get_dest_path(self, row):
"""Return temporary path where the runners should be downloaded to"""
return os.path.join(settings.CACHE_DIR, os.path.basename(row[self.COL_URL]))
get_installed_versions(self)
¶
List versions available locally
Source code in lutris/gui/dialogs/runner_install.py
def get_installed_versions(self):
"""List versions available locally"""
runner_path = os.path.join(settings.RUNNER_DIR, self.runner)
if not os.path.exists(runner_path):
return set()
return {
tuple(p.rsplit("-", 1))
for p in os.listdir(runner_path)
if "-" in p
}
get_progress(self, downloader, row)
¶
Update progress bar with download progress
Source code in lutris/gui/dialogs/runner_install.py
def get_progress(self, downloader, row):
"""Update progress bar with download progress"""
runner = row.runner
if downloader.state == downloader.CANCELLED:
return False
if downloader.state == downloader.ERROR:
self.cancel_install(row)
return False
row.install_progress.show()
downloader.check_progress()
percent_downloaded = downloader.progress_percentage
if percent_downloaded >= 1:
runner[self.COL_PROGRESS] = percent_downloaded
row.install_progress.set_fraction(percent_downloaded / 100)
else:
runner[self.COL_PROGRESS] = 1
row.install_progress.pulse()
row.install_progress.set_text = _("Downloading…")
if downloader.state == downloader.COMPLETED:
runner[self.COL_PROGRESS] = 99
row.install_progress.set_text = _("Extracting…")
self.on_runner_downloaded(row)
return False
return True
get_runner_path(self, version, arch)
¶
Return the local path where the runner is/will be installed
Source code in lutris/gui/dialogs/runner_install.py
def get_runner_path(self, version, arch):
"""Return the local path where the runner is/will be installed"""
return os.path.join(settings.RUNNER_DIR, self.runner, "{}-{}".format(version, arch))
get_usage_stats(self)
¶
Return the usage for each version
Source code in lutris/gui/dialogs/runner_install.py
def get_usage_stats(self):
"""Return the usage for each version"""
runner_games = get_games_by_runner(self.runner)
version_usage = defaultdict(list)
for db_game in runner_games:
if not db_game["installed"]:
continue
game = Game(db_game["id"])
version = game.config.runner_config["version"]
version_usage[version].append(db_game["id"])
return version_usage
install_runner(self, row)
¶
Download and install a runner version
Source code in lutris/gui/dialogs/runner_install.py
def install_runner(self, row):
"""Download and install a runner version"""
runner = row.runner
row.install_progress.set_fraction(0.0)
dest_path = self.get_dest_path(runner)
url = runner[self.COL_URL]
if not url:
ErrorDialog(_("Version %s is not longer available") % runner[self.COL_VER])
return
downloader = Downloader(runner[self.COL_URL], dest_path, overwrite=True)
GLib.timeout_add(100, self.get_progress, downloader, row)
self.installing[runner[self.COL_VER]] = downloader
downloader.start()
self.update_listboxrow(row)
on_cancel_install(self, widget, button, row)
¶
Source code in lutris/gui/dialogs/runner_install.py
def on_cancel_install(self, widget, button, row):
self.cancel_install(row)
on_destroy(self, _dialog, _data=None)
¶
Override delete handler to prevent closing while downloads are active
Source code in lutris/gui/dialogs/runner_install.py
def on_destroy(self, _dialog, _data=None):
"""Override delete handler to prevent closing while downloads are active"""
if self.installing:
return True
self.destroy()
return True
on_extracted(self, row_info, error)
¶
Called when a runner archive is extracted
Source code in lutris/gui/dialogs/runner_install.py
def on_extracted(self, row_info, error):
"""Called when a runner archive is extracted"""
if error or not row_info:
ErrorDialog(_("Failed to retrieve the runner archive"), parent=self)
return
src, row = row_info
runner = row.runner
os.remove(src)
runner[self.COL_PROGRESS] = 0
runner[self.COL_INSTALLED] = True
self.installing.pop(runner[self.COL_VER])
row.install_progress.set_text = ""
row.install_progress.set_fraction(0.0)
row.install_progress.hide()
self.update_listboxrow(row)
if self.runner == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
on_install_runner(self, _widget, _button, row)
¶
Source code in lutris/gui/dialogs/runner_install.py
def on_install_runner(self, _widget, _button, row):
self.install_runner(row)
on_installed_toggled(self, _widget, path)
¶
Source code in lutris/gui/dialogs/runner_install.py
def on_installed_toggled(self, _widget, path):
row = self.runner_store[path]
if row[self.COL_VER] in self.installing:
confirm_dlg = QuestionDialog(
{
"question": _("Do you want to cancel the download?"),
"title": _("Download starting"),
}
)
if confirm_dlg.result == confirm_dlg.YES:
self.cancel_install(row)
elif row[self.COL_INSTALLED]:
self.uninstall_runner(row)
else:
self.install_runner(row)
on_runner_downloaded(self, row)
¶
Handler called when a runner version is downloaded
Source code in lutris/gui/dialogs/runner_install.py
def on_runner_downloaded(self, row):
"""Handler called when a runner version is downloaded"""
runner = row.runner
version = runner[self.COL_VER]
architecture = runner[self.COL_ARCH]
logger.debug("Runner %s for %s has finished downloading", version, architecture)
src = self.get_dest_path(runner)
dst = self.get_runner_path(version, architecture)
GLib.timeout_add(100, self.progress_pulse, row)
jobs.AsyncCall(self.extract, self.on_extracted, src, dst, row)
on_show_apps_usage(self, _widget, _button, row)
¶
Return grid with games that uses this wine version
Source code in lutris/gui/dialogs/runner_install.py
def on_show_apps_usage(self, _widget, _button, row):
"""Return grid with games that uses this wine version"""
runner = row.runner
runner_version = "%s-%s" % (runner[self.COL_VER], runner[self.COL_ARCH])
runner_games = get_games_by_runner(self.runner)
apps = []
for db_game in runner_games:
if not db_game["installed"]:
continue
game = Game(db_game["id"])
version = game.config.runner_config["version"]
if version != runner_version:
continue
apps.append(game)
dialog = ShowAppsDialog(_("Wine version usage"), self.get_toplevel(), runner_version, apps)
dialog.run()
dialog.destroy()
on_uninstall_runner(self, widget, button, row)
¶
Source code in lutris/gui/dialogs/runner_install.py
def on_uninstall_runner(self, widget, button, row):
self.uninstall_runner(row)
populate_listboxrows(self, store)
¶
Source code in lutris/gui/dialogs/runner_install.py
def populate_listboxrows(self, store):
for runner in store:
row = Gtk.ListBoxRow()
row.runner = runner
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
row.hbox = hbox
chk_installed = Gtk.CheckButton()
chk_installed.set_sensitive(False)
chk_installed.set_active(runner[self.COL_INSTALLED])
hbox.pack_start(chk_installed, False, True, 0)
row.chk_installed = chk_installed
lbl_version = Gtk.Label(runner[self.COL_VER])
lbl_version.set_max_width_chars(20)
lbl_version.set_property("width-chars", 20)
lbl_version.set_halign(Gtk.Align.START)
hbox.pack_start(lbl_version, False, False, 5)
arch_label = Gtk.Label(runner[self.COL_ARCH])
arch_label.set_max_width_chars(8)
arch_label.set_halign(Gtk.Align.START)
hbox.pack_start(arch_label, False, True, 5)
install_progress = Gtk.ProgressBar()
install_progress.set_show_text(True)
hbox.pack_end(install_progress, True, True, 5)
row.install_progress = install_progress
if runner[self.COL_INSTALLED]:
# Check if there are apps installed, if so, show the view apps button
app_count = runner[self.COL_USAGE] or 0
if app_count > 0:
usage_button_text = gettext.ngettext(
"_View %d game",
"_View %d games",
app_count
) % app_count
usage_button = Gtk.Button.new_with_mnemonic(usage_button_text)
usage_button.connect("button_press_event", self.on_show_apps_usage, row)
hbox.pack_end(usage_button, False, True, 2)
button = Gtk.Button()
hbox.pack_end(button, False, True, 0)
hbox.reorder_child(button, 0)
row.install_uninstall_cancel_button = button
row.handler_id = None
row.add(hbox)
self.listbox.add(row)
row.show_all()
self.update_listboxrow(row)
populate_store(self)
¶
Return a ListStore populated with the runner versions
Source code in lutris/gui/dialogs/runner_install.py
def populate_store(self):
"""Return a ListStore populated with the runner versions"""
version_usage = self.get_usage_stats()
for version_info in reversed(self.runner_info["versions"]):
is_installed = os.path.exists(self.get_runner_path(version_info["version"], version_info["architecture"]))
games_using = version_usage.get("%(version)s-%(architecture)s" % version_info)
self.runner_store.append(
[
version_info["version"], version_info["architecture"], version_info["url"], is_installed, 0,
len(games_using) if games_using else 0
]
)
progress_pulse(self, row)
¶
Source code in lutris/gui/dialogs/runner_install.py
def progress_pulse(self, row):
runner = row.runner
row.install_progress.pulse()
return not runner[self.COL_INSTALLED]
runner_fetch_cb(self, runner_info, error)
¶
Clear the box and display versions from runner_info
Source code in lutris/gui/dialogs/runner_install.py
def runner_fetch_cb(self, runner_info, error):
"""Clear the box and display versions from runner_info"""
if error:
logger.error(error)
ErrorDialog(_("Unable to get runner versions: %s") % error)
return
self.runner_info = runner_info
remote_versions = {(v["version"], v["architecture"]) for v in self.runner_info["versions"]}
local_versions = self.get_installed_versions()
for local_version in local_versions - remote_versions:
self.runner_info["versions"].append({
"version": local_version[0],
"architecture": local_version[1],
"url": "",
})
if not self.runner_info:
ErrorDialog(_("Unable to get runner versions from lutris.net"))
return
for child_widget in self.vbox.get_children():
if child_widget.get_name() not in "GtkBox":
child_widget.destroy()
label = Gtk.Label.new(_("%s version management") % self.runner_info["name"])
self.vbox.add(label)
self.installing = {}
self.connect("response", self.on_destroy)
scrolled_listbox = Gtk.ScrolledWindow()
self.listbox = Gtk.ListBox()
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
scrolled_listbox.add(self.listbox)
self.vbox.pack_start(scrolled_listbox, True, True, 14)
self.populate_store()
self.show_all()
self.populate_listboxrows(self.runner_store)
uninstall_runner(self, row)
¶
Uninstall a runner version
Source code in lutris/gui/dialogs/runner_install.py
def uninstall_runner(self, row):
"""Uninstall a runner version"""
runner = row.runner
version = runner[self.COL_VER]
arch = runner[self.COL_ARCH]
system.remove_folder(self.get_runner_path(version, arch))
runner[self.COL_INSTALLED] = False
if self.runner == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
self.update_listboxrow(row)
update_listboxrow(self, row)
¶
Source code in lutris/gui/dialogs/runner_install.py
def update_listboxrow(self, row):
row.install_progress.set_visible(False)
row.chk_installed.set_active(row.runner[self.COL_INSTALLED])
button = row.install_uninstall_cancel_button
if row.handler_id is not None:
button.disconnect(row.handler_id)
row.handler_id = None
if row.runner[self.COL_VER] in self.installing:
button.set_label(_("Cancel"))
handler_id = button.connect("button_press_event", self.on_cancel_install, row)
else:
if row.runner[self.COL_INSTALLED]:
button.set_label(_("Uninstall"))
handler_id = button.connect("button_press_event", self.on_uninstall_runner, row)
else:
button.set_label(_("Install"))
handler_id = button.connect("button_press_event", self.on_install_runner, row)
row.install_uninstall_cancel_button = button
row.handler_id = handler_id
ShowAppsDialog (Dialog)
¶
Source code in lutris/gui/dialogs/runner_install.py
class ShowAppsDialog(Dialog):
def __init__(self, title, parent, runner_version, apps):
super().__init__(title, parent, Gtk.DialogFlags.MODAL)
self.add_buttons(
Gtk.STOCK_OK, Gtk.ResponseType.OK
)
self.set_default_size(400, 500)
label = Gtk.Label.new(_("Showing games using %s") % runner_version)
self.vbox.add(label)
scrolled_listbox = Gtk.ScrolledWindow()
listbox = Gtk.ListBox()
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
scrolled_listbox.add(listbox)
self.vbox.pack_start(scrolled_listbox, True, True, 14)
for app in apps:
row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
lbl_game = Gtk.Label(app.name)
lbl_game.set_halign(Gtk.Align.START)
hbox.pack_start(lbl_game, True, True, 5)
row.add(hbox)
listbox.add(row)
self.show_all()
__init__(self, title, parent, runner_version, apps)
special
¶
Source code in lutris/gui/dialogs/runner_install.py
def __init__(self, title, parent, runner_version, apps):
super().__init__(title, parent, Gtk.DialogFlags.MODAL)
self.add_buttons(
Gtk.STOCK_OK, Gtk.ResponseType.OK
)
self.set_default_size(400, 500)
label = Gtk.Label.new(_("Showing games using %s") % runner_version)
self.vbox.add(label)
scrolled_listbox = Gtk.ScrolledWindow()
listbox = Gtk.ListBox()
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
scrolled_listbox.add(listbox)
self.vbox.pack_start(scrolled_listbox, True, True, 14)
for app in apps:
row = Gtk.ListBoxRow()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
lbl_game = Gtk.Label(app.name)
lbl_game.set_halign(Gtk.Align.START)
hbox.pack_start(lbl_game, True, True, 5)
row.add(hbox)
listbox.add(row)
self.show_all()
uninstall_game
¶
RemoveGameDialog (Dialog)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
class RemoveGameDialog(Dialog):
def __init__(self, game_id, parent=None):
super().__init__(parent=parent)
self.set_size_request(640, 128)
self.game = Game(game_id)
container = Gtk.VBox(visible=True)
self.get_content_area().add(container)
title_label = Gtk.Label(visible=True)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
title_label.set_markup(_("<span font_desc='14'><b>Remove %s</b></span>") % gtk_safe(self.game.name))
container.pack_start(title_label, False, False, 4)
self.delete_label = Gtk.Label(visible=True)
self.delete_label.set_alignment(0, 0.5)
self.delete_label.set_markup(
_("Completely remove %s from the library?\nAll play time will be lost.") % self.game)
container.pack_start(self.delete_label, False, False, 4)
button_box = Gtk.HBox(visible=True)
button_box.set_margin_top(30)
style_context = button_box.get_style_context()
style_context.add_class("linked")
cancel_button = Gtk.Button(_("Cancel"), visible=True)
cancel_button.connect("clicked", self.on_close)
button_box.add(cancel_button)
self.remove_button = Gtk.Button(_("Remove"), visible=True)
self.remove_button.connect("clicked", self.on_remove_clicked)
button_box.add(self.remove_button)
container.pack_end(button_box, False, False, 0)
self.show()
def on_close(self, _button):
self.destroy()
def on_remove_clicked(self, button):
button.set_sensitive(False)
self.game.delete()
self.destroy()
__init__(self, game_id, parent=None)
special
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def __init__(self, game_id, parent=None):
super().__init__(parent=parent)
self.set_size_request(640, 128)
self.game = Game(game_id)
container = Gtk.VBox(visible=True)
self.get_content_area().add(container)
title_label = Gtk.Label(visible=True)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
title_label.set_markup(_("<span font_desc='14'><b>Remove %s</b></span>") % gtk_safe(self.game.name))
container.pack_start(title_label, False, False, 4)
self.delete_label = Gtk.Label(visible=True)
self.delete_label.set_alignment(0, 0.5)
self.delete_label.set_markup(
_("Completely remove %s from the library?\nAll play time will be lost.") % self.game)
container.pack_start(self.delete_label, False, False, 4)
button_box = Gtk.HBox(visible=True)
button_box.set_margin_top(30)
style_context = button_box.get_style_context()
style_context.add_class("linked")
cancel_button = Gtk.Button(_("Cancel"), visible=True)
cancel_button.connect("clicked", self.on_close)
button_box.add(cancel_button)
self.remove_button = Gtk.Button(_("Remove"), visible=True)
self.remove_button.connect("clicked", self.on_remove_clicked)
button_box.add(self.remove_button)
container.pack_end(button_box, False, False, 0)
self.show()
on_close(self, _button)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def on_close(self, _button):
self.destroy()
on_remove_clicked(self, button)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def on_remove_clicked(self, button):
button.set_sensitive(False)
self.game.delete()
self.destroy()
UninstallGameDialog (Dialog)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
class UninstallGameDialog(Dialog):
def __init__(self, game_id, parent=None):
super().__init__(parent=parent)
self.set_size_request(640, 128)
self.game = Game(game_id)
self.delete_files = False
container = Gtk.VBox(visible=True)
self.get_content_area().add(container)
title_label = Gtk.Label(visible=True)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
title_label.set_markup(_("<span font_desc='14'><b>Uninstall %s</b></span>") % gtk_safe(self.game.name))
container.pack_start(title_label, False, False, 4)
self.folder_label = Gtk.Label(visible=True)
self.folder_label.set_alignment(0, 0.5)
self.delete_button = Gtk.Button(_("Uninstall"), visible=True)
self.delete_button.connect("clicked", self.on_delete_clicked)
if not self.game.directory:
self.folder_label.set_markup(_("No file will be deleted"))
elif len(get_games(filters={"directory": self.game.directory})) > 1:
self.folder_label.set_markup(
_("The folder %s is used by other games and will be kept.") % self.game.directory)
elif is_removeable(self.game.directory):
self.delete_button.set_sensitive(False)
self.folder_label.set_markup(_("<i>Calculating size…</i>"))
AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory)
else:
self.folder_label.set_markup(
_("Content of %s are protected and will not be deleted.") % reverse_expanduser(self.game.directory)
)
container.pack_start(self.folder_label, False, False, 4)
self.confirm_delete_button = Gtk.CheckButton()
self.confirm_delete_button.set_active(True)
container.pack_start(self.confirm_delete_button, False, False, 4)
button_box = Gtk.HBox(visible=True)
button_box.set_margin_top(30)
style_context = button_box.get_style_context()
style_context.add_class("linked")
cancel_button = Gtk.Button(_("Cancel"), visible=True)
cancel_button.connect("clicked", self.on_close)
button_box.add(cancel_button)
button_box.add(self.delete_button)
container.pack_end(button_box, False, False, 0)
self.show()
def folder_size_cb(self, folder_size, error):
if error:
logger.error(error)
return
self.delete_files = True
self.delete_button.set_sensitive(True)
self.folder_label.hide()
self.confirm_delete_button.show()
self.confirm_delete_button.set_label(
_("Delete %s (%s)") % (
reverse_expanduser(self.game.directory),
human_size(folder_size)
)
)
def on_close(self, _button):
self.destroy()
def on_delete_clicked(self, button):
button.set_sensitive(False)
if not self.confirm_delete_button.get_active():
self.delete_files = False
if self.delete_files and not hasattr(self.game.runner, "no_game_remove_warning"):
dlg = QuestionDialog(
{
"question": _(
"Please confirm.\nEverything under <b>%s</b>\n"
"will be deleted."
) % gtk_safe(self.game.directory),
"title": _("Permanently delete files?"),
}
)
if dlg.result != Gtk.ResponseType.YES:
button.set_sensitive(True)
return
if self.delete_files:
self.folder_label.set_markup(_("Uninstalling game and deleting files..."))
else:
self.folder_label.set_markup(_("Uninstalling game..."))
self.game.remove(self.delete_files)
self.destroy()
__init__(self, game_id, parent=None)
special
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def __init__(self, game_id, parent=None):
super().__init__(parent=parent)
self.set_size_request(640, 128)
self.game = Game(game_id)
self.delete_files = False
container = Gtk.VBox(visible=True)
self.get_content_area().add(container)
title_label = Gtk.Label(visible=True)
title_label.set_line_wrap(True)
title_label.set_alignment(0, 0.5)
title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
title_label.set_markup(_("<span font_desc='14'><b>Uninstall %s</b></span>") % gtk_safe(self.game.name))
container.pack_start(title_label, False, False, 4)
self.folder_label = Gtk.Label(visible=True)
self.folder_label.set_alignment(0, 0.5)
self.delete_button = Gtk.Button(_("Uninstall"), visible=True)
self.delete_button.connect("clicked", self.on_delete_clicked)
if not self.game.directory:
self.folder_label.set_markup(_("No file will be deleted"))
elif len(get_games(filters={"directory": self.game.directory})) > 1:
self.folder_label.set_markup(
_("The folder %s is used by other games and will be kept.") % self.game.directory)
elif is_removeable(self.game.directory):
self.delete_button.set_sensitive(False)
self.folder_label.set_markup(_("<i>Calculating size…</i>"))
AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory)
else:
self.folder_label.set_markup(
_("Content of %s are protected and will not be deleted.") % reverse_expanduser(self.game.directory)
)
container.pack_start(self.folder_label, False, False, 4)
self.confirm_delete_button = Gtk.CheckButton()
self.confirm_delete_button.set_active(True)
container.pack_start(self.confirm_delete_button, False, False, 4)
button_box = Gtk.HBox(visible=True)
button_box.set_margin_top(30)
style_context = button_box.get_style_context()
style_context.add_class("linked")
cancel_button = Gtk.Button(_("Cancel"), visible=True)
cancel_button.connect("clicked", self.on_close)
button_box.add(cancel_button)
button_box.add(self.delete_button)
container.pack_end(button_box, False, False, 0)
self.show()
folder_size_cb(self, folder_size, error)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def folder_size_cb(self, folder_size, error):
if error:
logger.error(error)
return
self.delete_files = True
self.delete_button.set_sensitive(True)
self.folder_label.hide()
self.confirm_delete_button.show()
self.confirm_delete_button.set_label(
_("Delete %s (%s)") % (
reverse_expanduser(self.game.directory),
human_size(folder_size)
)
)
on_close(self, _button)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def on_close(self, _button):
self.destroy()
on_delete_clicked(self, button)
¶
Source code in lutris/gui/dialogs/uninstall_game.py
def on_delete_clicked(self, button):
button.set_sensitive(False)
if not self.confirm_delete_button.get_active():
self.delete_files = False
if self.delete_files and not hasattr(self.game.runner, "no_game_remove_warning"):
dlg = QuestionDialog(
{
"question": _(
"Please confirm.\nEverything under <b>%s</b>\n"
"will be deleted."
) % gtk_safe(self.game.directory),
"title": _("Permanently delete files?"),
}
)
if dlg.result != Gtk.ResponseType.YES:
button.set_sensitive(True)
return
if self.delete_files:
self.folder_label.set_markup(_("Uninstalling game and deleting files..."))
else:
self.folder_label.set_markup(_("Uninstalling game..."))
self.game.remove(self.delete_files)
self.destroy()
webconnect_dialog
¶
isort:skip_file
WebConnectDialog (Dialog)
¶
Login form for external services
Source code in lutris/gui/dialogs/webconnect_dialog.py
class WebConnectDialog(Dialog):
"""Login form for external services"""
def __init__(self, service, parent=None):
self.context = WebKit2.WebContext.new()
if "http_proxy" in os.environ:
proxy = WebKit2.NetworkProxySettings.new(os.environ["http_proxy"])
self.context.set_network_proxy_settings(WebKit2.NetworkProxyMode.CUSTOM, proxy)
WebKit2.CookieManager.set_persistent_storage(
self.context.get_cookie_manager(),
service.cookies_path,
WebKit2.CookiePersistentStorage(0),
)
self.service = service
super().__init__(title=service.name, parent=parent)
self.set_border_width(0)
self.set_default_size(390, 500)
self.webview = WebKit2.WebView.new_with_context(self.context)
self.webview.load_uri(service.login_url)
self.webview.connect("load-changed", self.on_navigation)
self.webview.connect("create", self.on_webview_popup)
self.vbox.pack_start(self.webview, True, True, 0) # pylint: disable=no-member
webkit_settings = self.webview.get_settings()
# Allow popups (Doesn't work...)
webkit_settings.set_enable_write_console_messages_to_stdout(True)
webkit_settings.set_allow_modal_dialogs(True)
# Enable developer options for troubleshooting (Can be disabled in
# releases)
webkit_settings.set_javascript_can_open_windows_automatically(True)
webkit_settings.set_enable_developer_extras(True)
self.show_all()
def enable_inspector(self):
"""If you want a full blown Webkit inspector, call this"""
inspector = self.webview.get_inspector()
inspector.show()
def on_navigation(self, widget, load_event):
if load_event == WebKit2.LoadEvent.FINISHED:
url = widget.get_uri()
if url in self.service.scripts:
script = self.service.scripts[url]
widget.run_javascript(script, None, None)
return True
if url.startswith(self.service.redirect_uri):
if self.service.requires_login_page:
resource = widget.get_main_resource()
resource.get_data(None, self._get_response_data_finish, None)
else:
self.service.login_callback(url)
self.destroy()
return True
def _get_response_data_finish(self, resource, result, user_data=None):
html_response = resource.get_data_finish(result)
self.service.login_callback(html_response)
self.destroy()
def on_webview_popup(self, widget, navigation_action):
"""Handles web popups created by this dialog's webview"""
uri = navigation_action.get_request().get_uri()
view = WebKit2.WebView.new_with_related_view(widget)
view.load_uri(uri)
popup_dialog = WebPopupDialog(view, parent=self)
popup_dialog.set_modal(True)
popup_dialog.show()
return view
__init__(self, service, parent=None)
special
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def __init__(self, service, parent=None):
self.context = WebKit2.WebContext.new()
if "http_proxy" in os.environ:
proxy = WebKit2.NetworkProxySettings.new(os.environ["http_proxy"])
self.context.set_network_proxy_settings(WebKit2.NetworkProxyMode.CUSTOM, proxy)
WebKit2.CookieManager.set_persistent_storage(
self.context.get_cookie_manager(),
service.cookies_path,
WebKit2.CookiePersistentStorage(0),
)
self.service = service
super().__init__(title=service.name, parent=parent)
self.set_border_width(0)
self.set_default_size(390, 500)
self.webview = WebKit2.WebView.new_with_context(self.context)
self.webview.load_uri(service.login_url)
self.webview.connect("load-changed", self.on_navigation)
self.webview.connect("create", self.on_webview_popup)
self.vbox.pack_start(self.webview, True, True, 0) # pylint: disable=no-member
webkit_settings = self.webview.get_settings()
# Allow popups (Doesn't work...)
webkit_settings.set_enable_write_console_messages_to_stdout(True)
webkit_settings.set_allow_modal_dialogs(True)
# Enable developer options for troubleshooting (Can be disabled in
# releases)
webkit_settings.set_javascript_can_open_windows_automatically(True)
webkit_settings.set_enable_developer_extras(True)
self.show_all()
enable_inspector(self)
¶
If you want a full blown Webkit inspector, call this
Source code in lutris/gui/dialogs/webconnect_dialog.py
def enable_inspector(self):
"""If you want a full blown Webkit inspector, call this"""
inspector = self.webview.get_inspector()
inspector.show()
on_navigation(self, widget, load_event)
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_navigation(self, widget, load_event):
if load_event == WebKit2.LoadEvent.FINISHED:
url = widget.get_uri()
if url in self.service.scripts:
script = self.service.scripts[url]
widget.run_javascript(script, None, None)
return True
if url.startswith(self.service.redirect_uri):
if self.service.requires_login_page:
resource = widget.get_main_resource()
resource.get_data(None, self._get_response_data_finish, None)
else:
self.service.login_callback(url)
self.destroy()
return True
on_webview_popup(self, widget, navigation_action)
¶
Handles web popups created by this dialog's webview
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_webview_popup(self, widget, navigation_action):
"""Handles web popups created by this dialog's webview"""
uri = navigation_action.get_request().get_uri()
view = WebKit2.WebView.new_with_related_view(widget)
view.load_uri(uri)
popup_dialog = WebPopupDialog(view, parent=self)
popup_dialog.set_modal(True)
popup_dialog.show()
return view
WebPopupDialog (Dialog)
¶
Dialog for handling web popups
Source code in lutris/gui/dialogs/webconnect_dialog.py
class WebPopupDialog(Dialog):
"""Dialog for handling web popups"""
def __init__(self, webview, parent=None):
# pylint: disable=no-member
self.parent = parent
super().__init__(title=_('Loading...'), parent=parent)
self.webview = webview
self.webview.connect("ready-to-show", self.on_ready_webview)
self.webview.connect("notify::title", self.on_available_webview_title)
self.webview.connect("create", self.on_new_webview_popup)
self.webview.connect("close", self.on_webview_close)
self.vbox.pack_start(self.webview, True, True, 0)
self.set_border_width(0)
self.set_default_size(390, 500)
def on_ready_webview(self, webview):
self.show_all()
def on_available_webview_title(self, webview, gparamstring):
self.set_title(webview.get_title())
def on_new_webview_popup(self, webview, navigation_action):
"""Handles web popups created by this dialog's webview"""
uri = navigation_action.get_request().get_uri()
view = WebKit2.WebView.new_with_related_view(webview)
view.load_uri(uri)
dialog = WebPopupDialog(view, parent=self)
dialog.set_modal(True)
dialog.show()
return view
def on_webview_close(self, webview):
self.destroy()
__init__(self, webview, parent=None)
special
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def __init__(self, webview, parent=None):
# pylint: disable=no-member
self.parent = parent
super().__init__(title=_('Loading...'), parent=parent)
self.webview = webview
self.webview.connect("ready-to-show", self.on_ready_webview)
self.webview.connect("notify::title", self.on_available_webview_title)
self.webview.connect("create", self.on_new_webview_popup)
self.webview.connect("close", self.on_webview_close)
self.vbox.pack_start(self.webview, True, True, 0)
self.set_border_width(0)
self.set_default_size(390, 500)
on_available_webview_title(self, webview, gparamstring)
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_available_webview_title(self, webview, gparamstring):
self.set_title(webview.get_title())
on_new_webview_popup(self, webview, navigation_action)
¶
Handles web popups created by this dialog's webview
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_new_webview_popup(self, webview, navigation_action):
"""Handles web popups created by this dialog's webview"""
uri = navigation_action.get_request().get_uri()
view = WebKit2.WebView.new_with_related_view(webview)
view.load_uri(uri)
dialog = WebPopupDialog(view, parent=self)
dialog.set_modal(True)
dialog.show()
return view
on_ready_webview(self, webview)
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_ready_webview(self, webview):
self.show_all()
on_webview_close(self, webview)
¶
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_webview_close(self, webview):
self.destroy()
installer
special
¶
file_box
¶
Widgets for the installer window
InstallerFileBox (VBox)
¶
Container for an installer file downloader / selector
Source code in lutris/gui/installer/file_box.py
class InstallerFileBox(Gtk.VBox):
"""Container for an installer file downloader / selector"""
__gsignals__ = {
"file-available": (GObject.SIGNAL_RUN_FIRST, None, ()),
"file-ready": (GObject.SIGNAL_RUN_FIRST, None, ()),
"file-unready": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, installer_file):
super().__init__()
self.installer_file = installer_file
self.cache_to_pga = self.installer_file.uses_pga_cache()
self.started = False
self.start_func = None
self.stop_func = None
self.state_label = None # Use this label to display status update
self.set_margin_left(12)
self.set_margin_right(12)
self.provider = self.installer_file.provider
self.file_provider_widget = None
self.add(self.get_widgets())
@property
def is_ready(self):
"""Whether the file is ready to be downloaded / fetched from its provider"""
if (
self.provider in ("user", "pga")
and not system.path_exists(self.installer_file.dest_file)
):
return False
return True
def get_download_progress(self):
"""Return the widget for the download progress bar"""
download_progress = DownloadProgressBox({
"url": self.installer_file.url,
"dest": self.installer_file.dest_file,
"referer": self.installer_file.referer
})
download_progress.connect("complete", self.on_download_complete)
download_progress.connect("cancel", self.on_download_cancelled)
download_progress.show()
if (
not self.installer_file.uses_pga_cache()
and system.path_exists(self.installer_file.dest_file)
):
os.remove(self.installer_file.dest_file)
return download_progress
def get_file_provider_widget(self):
"""Return the widget used to track progress of file"""
box = Gtk.VBox(spacing=6)
if self.provider == "download":
download_progress = self.get_download_progress()
self.start_func = download_progress.start
self.stop_func = download_progress.on_cancel_clicked
box.pack_start(download_progress, False, False, 0)
return box
if self.provider == "pga":
url_label = InstallerLabel("In cache: %s" % self.get_file_label(), wrap=False)
box.pack_start(url_label, False, False, 6)
return box
if self.provider == "user":
user_label = InstallerLabel(gtk_safe(self.installer_file.human_url))
box.pack_start(user_label, False, False, 0)
return box
if self.provider == "steam":
steam_installer = SteamInstaller(self.installer_file.url,
self.installer_file.id)
steam_installer.connect("steam-game-installed", self.on_download_complete)
steam_installer.connect("steam-state-changed", self.on_state_changed)
self.start_func = steam_installer.install_steam_game
self.stop_func = steam_installer.stop_func
steam_box = Gtk.HBox(spacing=6)
info_box = Gtk.VBox(spacing=6)
steam_label = InstallerLabel(_("Steam game <b>{appid}</b>").format(
appid=steam_installer.appid
))
info_box.add(steam_label)
self.state_label = InstallerLabel("")
info_box.add(self.state_label)
steam_box.add(info_box)
return steam_box
raise ValueError("Invalid provider %s" % self.provider)
def get_file_label(self):
"""Return a human readable label for installer files"""
url = self.installer_file.url
if url.startswith("http"):
parsed = urlparse(url)
label = _("{file} on {host}").format(file=self.installer_file.filename, host=parsed.netloc)
elif url.startswith("N/A"):
label = url[3:].lstrip(":")
else:
label = url
return add_url_tags(gtk_safe(label))
def get_combobox_model(self):
""""Return the combobox's model"""
model = Gtk.ListStore(str, str)
if "download" in self.installer_file.providers:
model.append(["download", "Download"])
if "pga" in self.installer_file.providers:
model.append(["pga", "Use Cache"])
if "steam" in self.installer_file.providers:
model.append(["steam", "Steam"])
model.append(["user", "Select File"])
return model
def get_combobox(self):
"""Return the combobox widget to select file source"""
combobox = Gtk.ComboBox.new_with_model(self.get_combobox_model())
combobox.set_id_column(0)
renderer_text = Gtk.CellRendererText()
combobox.pack_start(renderer_text, True)
combobox.add_attribute(renderer_text, "text", 1)
combobox.connect("changed", self.on_source_changed)
combobox.set_active_id(self.provider)
return combobox
def replace_file_provider_widget(self):
"""Replace the file provider label and the source button with the actual widget"""
self.file_provider_widget.destroy()
widget_box = self.get_children()[0]
if self.started:
self.file_provider_widget = self.get_file_provider_widget()
# Also remove the the source button
for child in widget_box.get_children():
child.destroy()
else:
self.file_provider_widget = self.get_file_provider_label()
widget_box.pack_start(self.file_provider_widget, True, True, 0)
widget_box.reorder_child(self.file_provider_widget, 0)
widget_box.show_all()
def on_source_changed(self, combobox):
"""Change the source to a new provider, emit a new state"""
tree_iter = combobox.get_active_iter()
if tree_iter is None:
return
model = combobox.get_model()
source = model[tree_iter][0]
if source == self.provider:
return
self.provider = source
self.replace_file_provider_widget()
if self.provider == "user":
self.emit("file-unready")
else:
self.emit("file-ready")
def get_file_provider_label(self):
"""Return the label displayed before the download starts"""
if self.provider == "user":
box = Gtk.VBox(spacing=6)
label = InstallerLabel(self.get_file_label())
label.props.can_focus = True
box.pack_start(label, False, False, 0)
location_entry = FileChooserEntry(
self.installer_file.human_url,
Gtk.FileChooserAction.OPEN,
path=None
)
location_entry.entry.connect("changed", self.on_location_changed)
location_entry.show()
box.pack_start(location_entry, False, False, 0)
if self.installer_file.uses_pga_cache(create=True):
cache_option = Gtk.CheckButton(_("Cache file for future installations"))
cache_option.set_active(self.cache_to_pga)
cache_option.connect("toggled", self.on_user_file_cached)
box.pack_start(cache_option, False, False, 0)
return box
return InstallerLabel(self.get_file_label())
def get_widgets(self):
"""Return the widget with the source of the file and a way to change its source"""
box = Gtk.HBox(
spacing=12,
margin_top=6,
margin_bottom=6
)
self.file_provider_widget = self.get_file_provider_label()
box.pack_start(self.file_provider_widget, True, True, 0)
source_box = Gtk.HBox()
source_box.props.valign = Gtk.Align.START
box.pack_start(source_box, False, False, 0)
source_box.pack_start(InstallerLabel(_("Source:")), False, False, 0)
combobox = self.get_combobox()
source_box.pack_start(combobox, False, False, 0)
return box
def on_location_changed(self, widget):
"""Open a file picker when the browse button is clicked"""
file_path = os.path.expanduser(widget.get_text())
self.installer_file.dest_file = file_path
if system.path_exists(file_path):
self.emit("file-ready")
else:
self.emit("file-unready")
def on_user_file_cached(self, checkbutton):
"""Enable or disable caching of user provided files"""
self.cache_to_pga = checkbutton.get_active()
def on_state_changed(self, _widget, state):
"""Update the state label with a new state"""
self.state_label.set_text(state)
def start(self):
"""Starts the download of the file"""
self.started = True
self.installer_file.prepare()
self.replace_file_provider_widget()
if self.provider in ("pga", "user") and self.is_ready:
self.emit("file-available")
self.cache_file()
return
if self.start_func:
return self.start_func()
def cache_file(self):
"""Copy file to the PGA cache"""
if self.cache_to_pga:
save_to_cache(self.installer_file.dest_file, self.installer_file.cache_path)
def on_download_cancelled(self, downloader):
"""Handle cancellation of installers"""
logger.error("Download from %s cancelled", downloader)
downloader.set_retry_button()
def on_download_complete(self, widget, _data=None):
"""Action called on a completed download."""
logger.info("Download completed")
if isinstance(widget, SteamInstaller):
self.installer_file.dest_file = widget.get_steam_data_path()
else:
self.cache_file()
self.emit("file-available")
is_ready
property
readonly
¶
Whether the file is ready to be downloaded / fetched from its provider
__init__(self, installer_file)
special
¶
Source code in lutris/gui/installer/file_box.py
def __init__(self, installer_file):
super().__init__()
self.installer_file = installer_file
self.cache_to_pga = self.installer_file.uses_pga_cache()
self.started = False
self.start_func = None
self.stop_func = None
self.state_label = None # Use this label to display status update
self.set_margin_left(12)
self.set_margin_right(12)
self.provider = self.installer_file.provider
self.file_provider_widget = None
self.add(self.get_widgets())
cache_file(self)
¶
Copy file to the PGA cache
Source code in lutris/gui/installer/file_box.py
def cache_file(self):
"""Copy file to the PGA cache"""
if self.cache_to_pga:
save_to_cache(self.installer_file.dest_file, self.installer_file.cache_path)
get_combobox(self)
¶
Return the combobox widget to select file source
Source code in lutris/gui/installer/file_box.py
def get_combobox(self):
"""Return the combobox widget to select file source"""
combobox = Gtk.ComboBox.new_with_model(self.get_combobox_model())
combobox.set_id_column(0)
renderer_text = Gtk.CellRendererText()
combobox.pack_start(renderer_text, True)
combobox.add_attribute(renderer_text, "text", 1)
combobox.connect("changed", self.on_source_changed)
combobox.set_active_id(self.provider)
return combobox
get_combobox_model(self)
¶
"Return the combobox's model
Source code in lutris/gui/installer/file_box.py
def get_combobox_model(self):
""""Return the combobox's model"""
model = Gtk.ListStore(str, str)
if "download" in self.installer_file.providers:
model.append(["download", "Download"])
if "pga" in self.installer_file.providers:
model.append(["pga", "Use Cache"])
if "steam" in self.installer_file.providers:
model.append(["steam", "Steam"])
model.append(["user", "Select File"])
return model
get_download_progress(self)
¶
Return the widget for the download progress bar
Source code in lutris/gui/installer/file_box.py
def get_download_progress(self):
"""Return the widget for the download progress bar"""
download_progress = DownloadProgressBox({
"url": self.installer_file.url,
"dest": self.installer_file.dest_file,
"referer": self.installer_file.referer
})
download_progress.connect("complete", self.on_download_complete)
download_progress.connect("cancel", self.on_download_cancelled)
download_progress.show()
if (
not self.installer_file.uses_pga_cache()
and system.path_exists(self.installer_file.dest_file)
):
os.remove(self.installer_file.dest_file)
return download_progress
get_file_label(self)
¶
Return a human readable label for installer files
Source code in lutris/gui/installer/file_box.py
def get_file_label(self):
"""Return a human readable label for installer files"""
url = self.installer_file.url
if url.startswith("http"):
parsed = urlparse(url)
label = _("{file} on {host}").format(file=self.installer_file.filename, host=parsed.netloc)
elif url.startswith("N/A"):
label = url[3:].lstrip(":")
else:
label = url
return add_url_tags(gtk_safe(label))
get_file_provider_label(self)
¶
Return the label displayed before the download starts
Source code in lutris/gui/installer/file_box.py
def get_file_provider_label(self):
"""Return the label displayed before the download starts"""
if self.provider == "user":
box = Gtk.VBox(spacing=6)
label = InstallerLabel(self.get_file_label())
label.props.can_focus = True
box.pack_start(label, False, False, 0)
location_entry = FileChooserEntry(
self.installer_file.human_url,
Gtk.FileChooserAction.OPEN,
path=None
)
location_entry.entry.connect("changed", self.on_location_changed)
location_entry.show()
box.pack_start(location_entry, False, False, 0)
if self.installer_file.uses_pga_cache(create=True):
cache_option = Gtk.CheckButton(_("Cache file for future installations"))
cache_option.set_active(self.cache_to_pga)
cache_option.connect("toggled", self.on_user_file_cached)
box.pack_start(cache_option, False, False, 0)
return box
return InstallerLabel(self.get_file_label())
get_file_provider_widget(self)
¶
Return the widget used to track progress of file
Source code in lutris/gui/installer/file_box.py
def get_file_provider_widget(self):
"""Return the widget used to track progress of file"""
box = Gtk.VBox(spacing=6)
if self.provider == "download":
download_progress = self.get_download_progress()
self.start_func = download_progress.start
self.stop_func = download_progress.on_cancel_clicked
box.pack_start(download_progress, False, False, 0)
return box
if self.provider == "pga":
url_label = InstallerLabel("In cache: %s" % self.get_file_label(), wrap=False)
box.pack_start(url_label, False, False, 6)
return box
if self.provider == "user":
user_label = InstallerLabel(gtk_safe(self.installer_file.human_url))
box.pack_start(user_label, False, False, 0)
return box
if self.provider == "steam":
steam_installer = SteamInstaller(self.installer_file.url,
self.installer_file.id)
steam_installer.connect("steam-game-installed", self.on_download_complete)
steam_installer.connect("steam-state-changed", self.on_state_changed)
self.start_func = steam_installer.install_steam_game
self.stop_func = steam_installer.stop_func
steam_box = Gtk.HBox(spacing=6)
info_box = Gtk.VBox(spacing=6)
steam_label = InstallerLabel(_("Steam game <b>{appid}</b>").format(
appid=steam_installer.appid
))
info_box.add(steam_label)
self.state_label = InstallerLabel("")
info_box.add(self.state_label)
steam_box.add(info_box)
return steam_box
raise ValueError("Invalid provider %s" % self.provider)
get_widgets(self)
¶
Return the widget with the source of the file and a way to change its source
Source code in lutris/gui/installer/file_box.py
def get_widgets(self):
"""Return the widget with the source of the file and a way to change its source"""
box = Gtk.HBox(
spacing=12,
margin_top=6,
margin_bottom=6
)
self.file_provider_widget = self.get_file_provider_label()
box.pack_start(self.file_provider_widget, True, True, 0)
source_box = Gtk.HBox()
source_box.props.valign = Gtk.Align.START
box.pack_start(source_box, False, False, 0)
source_box.pack_start(InstallerLabel(_("Source:")), False, False, 0)
combobox = self.get_combobox()
source_box.pack_start(combobox, False, False, 0)
return box
on_download_cancelled(self, downloader)
¶
Handle cancellation of installers
Source code in lutris/gui/installer/file_box.py
def on_download_cancelled(self, downloader):
"""Handle cancellation of installers"""
logger.error("Download from %s cancelled", downloader)
downloader.set_retry_button()
on_download_complete(self, widget, _data=None)
¶
Action called on a completed download.
Source code in lutris/gui/installer/file_box.py
def on_download_complete(self, widget, _data=None):
"""Action called on a completed download."""
logger.info("Download completed")
if isinstance(widget, SteamInstaller):
self.installer_file.dest_file = widget.get_steam_data_path()
else:
self.cache_file()
self.emit("file-available")
on_location_changed(self, widget)
¶
Open a file picker when the browse button is clicked
Source code in lutris/gui/installer/file_box.py
def on_location_changed(self, widget):
"""Open a file picker when the browse button is clicked"""
file_path = os.path.expanduser(widget.get_text())
self.installer_file.dest_file = file_path
if system.path_exists(file_path):
self.emit("file-ready")
else:
self.emit("file-unready")
on_source_changed(self, combobox)
¶
Change the source to a new provider, emit a new state
Source code in lutris/gui/installer/file_box.py
def on_source_changed(self, combobox):
"""Change the source to a new provider, emit a new state"""
tree_iter = combobox.get_active_iter()
if tree_iter is None:
return
model = combobox.get_model()
source = model[tree_iter][0]
if source == self.provider:
return
self.provider = source
self.replace_file_provider_widget()
if self.provider == "user":
self.emit("file-unready")
else:
self.emit("file-ready")
on_state_changed(self, _widget, state)
¶
Update the state label with a new state
Source code in lutris/gui/installer/file_box.py
def on_state_changed(self, _widget, state):
"""Update the state label with a new state"""
self.state_label.set_text(state)
on_user_file_cached(self, checkbutton)
¶
Enable or disable caching of user provided files
Source code in lutris/gui/installer/file_box.py
def on_user_file_cached(self, checkbutton):
"""Enable or disable caching of user provided files"""
self.cache_to_pga = checkbutton.get_active()
replace_file_provider_widget(self)
¶
Replace the file provider label and the source button with the actual widget
Source code in lutris/gui/installer/file_box.py
def replace_file_provider_widget(self):
"""Replace the file provider label and the source button with the actual widget"""
self.file_provider_widget.destroy()
widget_box = self.get_children()[0]
if self.started:
self.file_provider_widget = self.get_file_provider_widget()
# Also remove the the source button
for child in widget_box.get_children():
child.destroy()
else:
self.file_provider_widget = self.get_file_provider_label()
widget_box.pack_start(self.file_provider_widget, True, True, 0)
widget_box.reorder_child(self.file_provider_widget, 0)
widget_box.show_all()
start(self)
¶
Starts the download of the file
Source code in lutris/gui/installer/file_box.py
def start(self):
"""Starts the download of the file"""
self.started = True
self.installer_file.prepare()
self.replace_file_provider_widget()
if self.provider in ("pga", "user") and self.is_ready:
self.emit("file-available")
self.cache_file()
return
if self.start_func:
return self.start_func()
files_box
¶
InstallerFilesBox (ListBox)
¶
List box presenting all files needed for an installer
Source code in lutris/gui/installer/files_box.py
class InstallerFilesBox(Gtk.ListBox):
"""List box presenting all files needed for an installer"""
max_downloads = 3
__gsignals__ = {
"files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
"files-available": (GObject.SIGNAL_RUN_LAST, None, ())
}
def __init__(self, installer, parent):
super().__init__()
self.parent = parent
self.installer = installer
self.installer_files = installer.files
self.ready_files = set()
self.available_files = set()
self.installer_files_boxes = {}
self._file_queue = []
for installer_file in installer.files:
installer_file_box = InstallerFileBox(installer_file)
installer_file_box.connect("file-ready", self.on_file_ready)
installer_file_box.connect("file-unready", self.on_file_unready)
installer_file_box.connect("file-available", self.on_file_available)
self.installer_files_boxes[installer_file.id] = installer_file_box
self.add(installer_file_box)
if installer_file_box.is_ready:
self.ready_files.add(installer_file.id)
self.show_all()
self.check_files_ready()
def start_all(self):
"""Iterates through installer files while keeping the number
of simultaneously downloaded files down to a maximum number"""
started_downloads = 0
for file_id, file_entry in self.installer_files_boxes.items():
if file_entry.provider == "download":
started_downloads += 1
if started_downloads <= self.max_downloads:
file_entry.start()
else:
self._file_queue.append(file_id)
else:
file_entry.start()
def stop_all(self):
"""Stops all ongoing files gathering.
Iterates through installer files, and call the "stop" command
if they've been started and not available yet.
"""
self._file_queue.clear()
for file_id, file_box in self.installer_files_boxes.items():
if file_box.started and file_id not in self.available_files and file_box.stop_func is not None:
file_box.stop_func()
@property
def is_ready(self):
"""Return True if all files are ready to be fetched"""
return len(self.ready_files) == len(self.installer.files)
def check_files_ready(self):
"""Checks if all installer files are ready and emit a signal if so"""
if self.is_ready:
self.emit("files-ready", self.is_ready)
else:
logger.info("Waiting for user to provide files")
def on_file_ready(self, widget):
"""Fired when a file has a valid provider.
If the file is user provided, it must set to a valid path.
"""
file_id = widget.installer_file.id
self.ready_files.add(file_id)
self.check_files_ready()
def on_file_unready(self, widget):
"""Fired when a file can't be provided.
Blocks the installer from continuing.
"""
file_id = widget.installer_file.id
self.ready_files.remove(file_id)
self.check_files_ready()
def on_file_available(self, widget):
"""A new file is available"""
file_id = widget.installer_file.id
logger.debug("%s is available", file_id)
self.available_files.add(file_id)
if self._file_queue:
next_file_id = self._file_queue.pop()
self.installer_files_boxes[next_file_id].start()
if len(self.available_files) == len(self.installer_files):
logger.info("All files available")
self.emit("files-available")
def get_game_files(self):
"""Return a mapping of the local files usable by the interpreter"""
return {
installer_file.id: installer_file.dest_file
for installer_file in self.installer_files
}
is_ready
property
readonly
¶
Return True if all files are ready to be fetched
max_downloads
¶
__init__(self, installer, parent)
special
¶
Source code in lutris/gui/installer/files_box.py
def __init__(self, installer, parent):
super().__init__()
self.parent = parent
self.installer = installer
self.installer_files = installer.files
self.ready_files = set()
self.available_files = set()
self.installer_files_boxes = {}
self._file_queue = []
for installer_file in installer.files:
installer_file_box = InstallerFileBox(installer_file)
installer_file_box.connect("file-ready", self.on_file_ready)
installer_file_box.connect("file-unready", self.on_file_unready)
installer_file_box.connect("file-available", self.on_file_available)
self.installer_files_boxes[installer_file.id] = installer_file_box
self.add(installer_file_box)
if installer_file_box.is_ready:
self.ready_files.add(installer_file.id)
self.show_all()
self.check_files_ready()
check_files_ready(self)
¶
Checks if all installer files are ready and emit a signal if so
Source code in lutris/gui/installer/files_box.py
def check_files_ready(self):
"""Checks if all installer files are ready and emit a signal if so"""
if self.is_ready:
self.emit("files-ready", self.is_ready)
else:
logger.info("Waiting for user to provide files")
get_game_files(self)
¶
Return a mapping of the local files usable by the interpreter
Source code in lutris/gui/installer/files_box.py
def get_game_files(self):
"""Return a mapping of the local files usable by the interpreter"""
return {
installer_file.id: installer_file.dest_file
for installer_file in self.installer_files
}
on_file_available(self, widget)
¶
A new file is available
Source code in lutris/gui/installer/files_box.py
def on_file_available(self, widget):
"""A new file is available"""
file_id = widget.installer_file.id
logger.debug("%s is available", file_id)
self.available_files.add(file_id)
if self._file_queue:
next_file_id = self._file_queue.pop()
self.installer_files_boxes[next_file_id].start()
if len(self.available_files) == len(self.installer_files):
logger.info("All files available")
self.emit("files-available")
on_file_ready(self, widget)
¶
Fired when a file has a valid provider. If the file is user provided, it must set to a valid path.
Source code in lutris/gui/installer/files_box.py
def on_file_ready(self, widget):
"""Fired when a file has a valid provider.
If the file is user provided, it must set to a valid path.
"""
file_id = widget.installer_file.id
self.ready_files.add(file_id)
self.check_files_ready()
on_file_unready(self, widget)
¶
Fired when a file can't be provided. Blocks the installer from continuing.
Source code in lutris/gui/installer/files_box.py
def on_file_unready(self, widget):
"""Fired when a file can't be provided.
Blocks the installer from continuing.
"""
file_id = widget.installer_file.id
self.ready_files.remove(file_id)
self.check_files_ready()
start_all(self)
¶
Iterates through installer files while keeping the number of simultaneously downloaded files down to a maximum number
Source code in lutris/gui/installer/files_box.py
def start_all(self):
"""Iterates through installer files while keeping the number
of simultaneously downloaded files down to a maximum number"""
started_downloads = 0
for file_id, file_entry in self.installer_files_boxes.items():
if file_entry.provider == "download":
started_downloads += 1
if started_downloads <= self.max_downloads:
file_entry.start()
else:
self._file_queue.append(file_id)
else:
file_entry.start()
stop_all(self)
¶
Stops all ongoing files gathering. Iterates through installer files, and call the "stop" command if they've been started and not available yet.
Source code in lutris/gui/installer/files_box.py
def stop_all(self):
"""Stops all ongoing files gathering.
Iterates through installer files, and call the "stop" command
if they've been started and not available yet.
"""
self._file_queue.clear()
for file_id, file_box in self.installer_files_boxes.items():
if file_box.started and file_id not in self.available_files and file_box.stop_func is not None:
file_box.stop_func()
script_box
¶
InstallerScriptBox (VBox)
¶
Box displaying the details of a script, with associated action buttons
Source code in lutris/gui/installer/script_box.py
class InstallerScriptBox(Gtk.VBox):
"""Box displaying the details of a script, with associated action buttons"""
def __init__(self, script, parent=None, revealed=False):
super().__init__()
self.script = script
self.parent = parent
self.revealer = None
self.set_margin_left(12)
self.set_margin_right(12)
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
box.pack_start(self.get_infobox(), True, True, 0)
box.add(self.get_install_button())
self.add(box)
self.add(self.get_revealer(revealed))
def get_rating(self):
"""Return a string representation of the API rating"""
return ""
def get_infobox(self):
"""Return the central information box"""
info_box = Gtk.VBox(spacing=6)
title_box = Gtk.HBox(spacing=6)
runner_label = InstallerLabel("%s" % self.script["runner"])
runner_label.get_style_context().add_class("info-pill")
title_box.pack_start(runner_label, False, False, 0)
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
title_box.pack_start(InstallerLabel(""), True, True, 0)
rating_label = InstallerLabel(self.get_rating())
rating_label.set_alignment(1, 0.5)
title_box.pack_end(rating_label, False, False, 0)
info_box.add(title_box)
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
return info_box
def get_revealer(self, revealed):
"""Return the revelaer widget"""
self.revealer = Gtk.Revealer()
self.revealer.add(self.get_notes())
self.revealer.set_reveal_child(revealed)
return self.revealer
def get_install_button(self):
"""Return the install button widget"""
align = Gtk.Alignment()
align.set(0, 0, 0, 0)
install_button = Gtk.Button(_("Install"))
install_button.connect("clicked", self.on_install_clicked)
align.add(install_button)
return align
def get_notes(self):
"""Return the notes widget"""
notes = self.script["notes"].strip()
if not notes:
return Gtk.Alignment()
notes_label = InstallerLabel(notes)
notes_label.set_margin_top(12)
notes_label.set_margin_bottom(12)
notes_label.set_margin_right(12)
notes_label.set_margin_left(12)
return notes_label
def reveal(self, reveal=True):
"""Show or hide the information in the revealer"""
if self.revealer:
self.revealer.set_reveal_child(reveal)
def on_install_clicked(self, _widget):
"""Handler to notify the parent of the selected installer"""
self.parent.emit("installer-selected", self.script["version"])
__init__(self, script, parent=None, revealed=False)
special
¶
Source code in lutris/gui/installer/script_box.py
def __init__(self, script, parent=None, revealed=False):
super().__init__()
self.script = script
self.parent = parent
self.revealer = None
self.set_margin_left(12)
self.set_margin_right(12)
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
box.pack_start(self.get_infobox(), True, True, 0)
box.add(self.get_install_button())
self.add(box)
self.add(self.get_revealer(revealed))
get_infobox(self)
¶
Return the central information box
Source code in lutris/gui/installer/script_box.py
def get_infobox(self):
"""Return the central information box"""
info_box = Gtk.VBox(spacing=6)
title_box = Gtk.HBox(spacing=6)
runner_label = InstallerLabel("%s" % self.script["runner"])
runner_label.get_style_context().add_class("info-pill")
title_box.pack_start(runner_label, False, False, 0)
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
title_box.pack_start(InstallerLabel(""), True, True, 0)
rating_label = InstallerLabel(self.get_rating())
rating_label.set_alignment(1, 0.5)
title_box.pack_end(rating_label, False, False, 0)
info_box.add(title_box)
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
return info_box
get_install_button(self)
¶
Return the install button widget
Source code in lutris/gui/installer/script_box.py
def get_install_button(self):
"""Return the install button widget"""
align = Gtk.Alignment()
align.set(0, 0, 0, 0)
install_button = Gtk.Button(_("Install"))
install_button.connect("clicked", self.on_install_clicked)
align.add(install_button)
return align
get_notes(self)
¶
Return the notes widget
Source code in lutris/gui/installer/script_box.py
def get_notes(self):
"""Return the notes widget"""
notes = self.script["notes"].strip()
if not notes:
return Gtk.Alignment()
notes_label = InstallerLabel(notes)
notes_label.set_margin_top(12)
notes_label.set_margin_bottom(12)
notes_label.set_margin_right(12)
notes_label.set_margin_left(12)
return notes_label
get_rating(self)
¶
Return a string representation of the API rating
Source code in lutris/gui/installer/script_box.py
def get_rating(self):
"""Return a string representation of the API rating"""
return ""
get_revealer(self, revealed)
¶
Return the revelaer widget
Source code in lutris/gui/installer/script_box.py
def get_revealer(self, revealed):
"""Return the revelaer widget"""
self.revealer = Gtk.Revealer()
self.revealer.add(self.get_notes())
self.revealer.set_reveal_child(revealed)
return self.revealer
on_install_clicked(self, _widget)
¶
Handler to notify the parent of the selected installer
Source code in lutris/gui/installer/script_box.py
def on_install_clicked(self, _widget):
"""Handler to notify the parent of the selected installer"""
self.parent.emit("installer-selected", self.script["version"])
reveal(self, reveal=True)
¶
Show or hide the information in the revealer
Source code in lutris/gui/installer/script_box.py
def reveal(self, reveal=True):
"""Show or hide the information in the revealer"""
if self.revealer:
self.revealer.set_reveal_child(reveal)
script_picker
¶
InstallerPicker (ListBox)
¶
List box to pick between several installers
Source code in lutris/gui/installer/script_picker.py
class InstallerPicker(Gtk.ListBox):
"""List box to pick between several installers"""
__gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}
def __init__(self, scripts):
super().__init__()
revealed = True
for script in scripts:
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
revealed = False # Only reveal the first installer.
self.connect('row-selected', self.on_activate)
self.show_all()
@staticmethod
def on_activate(widget, row):
"""Handler for hiding and showing the revealers in children"""
for script_box_row in widget:
script_box = script_box_row.get_children()[0]
script_box.reveal(False)
installer_row = row.get_children()[0]
installer_row.reveal()
__init__(self, scripts)
special
¶
Source code in lutris/gui/installer/script_picker.py
def __init__(self, scripts):
super().__init__()
revealed = True
for script in scripts:
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
revealed = False # Only reveal the first installer.
self.connect('row-selected', self.on_activate)
self.show_all()
on_activate(widget, row)
staticmethod
¶
Handler for hiding and showing the revealers in children
Source code in lutris/gui/installer/script_picker.py
@staticmethod
def on_activate(widget, row):
"""Handler for hiding and showing the revealers in children"""
for script_box_row in widget:
script_box = script_box_row.get_children()[0]
script_box.reveal(False)
installer_row = row.get_children()[0]
installer_row.reveal()
widgets
¶
InstallerLabel (Label)
¶
A label for installers
Source code in lutris/gui/installer/widgets.py
class InstallerLabel(Gtk.Label):
"""A label for installers"""
def __init__(self, text, wrap=True):
super().__init__()
if wrap:
self.set_line_wrap(True)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
else:
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.set_alignment(0, 0.5)
self.set_margin_right(12)
self.set_markup(text)
self.props.can_focus = False
self.set_tooltip_text(text)
__init__(self, text, wrap=True)
special
¶
Source code in lutris/gui/installer/widgets.py
def __init__(self, text, wrap=True):
super().__init__()
if wrap:
self.set_line_wrap(True)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
else:
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.set_alignment(0, 0.5)
self.set_margin_right(12)
self.set_markup(text)
self.props.can_focus = False
self.set_tooltip_text(text)
installerwindow
¶
Window used for game installers
InstallerWindow (BaseApplicationWindow)
¶
GUI for the install process.
Source code in lutris/gui/installerwindow.py
class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public-methods
"""GUI for the install process."""
def __init__(
self,
installers,
service=None,
appid=None,
application=None,
is_update=False
):
super().__init__(application=application)
self.set_default_size(540, 320)
self.installers = installers
self.config = {}
self.service = service
self.appid = appid
self.install_in_progress = False
self.interpreter = None
self.is_update = is_update
self.log_buffer = None
self.log_textview = None
self._cancel_files_func = None
self.title_label = InstallerLabel()
self.title_label.set_selectable(False)
self.vbox.add(self.title_label)
self.status_label = InstallerLabel()
self.status_label.set_max_width_chars(80)
self.status_label.set_property("wrap", True)
self.status_label.set_selectable(True)
self.vbox.add(self.status_label)
self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.vbox.pack_start(self.widget_box, True, True, 0)
self.vbox.add(Gtk.HSeparator())
button_box = Gtk.Box()
self.cache_button = Gtk.Button(_("Cache"))
self.cache_button.connect("clicked", self.on_cache_clicked)
button_box.add(self.cache_button)
self.action_buttons = Gtk.Box(spacing=6)
action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
action_buttons_alignment.add(self.action_buttons)
button_box.pack_end(action_buttons_alignment, True, True, 0)
self.vbox.pack_start(button_box, False, True, 0)
self.cancel_button = self.add_button(
_("C_ancel"), self.confirm_cancel, tooltip=_("Abort and revert the installation")
)
self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
self.source_button = self.add_button(_("_View source"), self.on_source_clicked)
self.install_button = self.add_button(_("_Install"), self.on_install_clicked)
self.continue_button = self.add_button(_("_Continue"))
self.play_button = self.add_button(_("_Launch"), self.launch_game)
self.close_button = self.add_button(_("_Close"), self.on_destroy)
self.continue_handler = None
self.clean_widgets()
self.show_all()
self.close_button.hide()
self.play_button.hide()
self.install_button.hide()
self.source_button.hide()
self.eject_button.hide()
self.continue_button.hide()
self.install_in_progress = True
self.widget_box.show()
self.title_label.show()
self.choose_installer()
self.present()
def add_button(self, label, handler=None, tooltip=None):
"""Add a button to the action buttons box"""
button = Gtk.Button.new_with_mnemonic(label)
if tooltip:
button.set_tooltip_text(tooltip)
if handler:
button.connect("clicked", handler)
self.action_buttons.add(button)
return button
def validate_scripts(self):
"""Auto-fixes some script aspects and checks for mandatory fields"""
if not self.installers:
raise ScriptingError("No installer available")
for script in self.installers:
for item in ["description", "notes"]:
script[item] = script.get(item) or ""
for item in ["name", "runner", "version"]:
if item not in script:
logger.error("Invalid script: %s", script)
raise ScriptingError(_('Missing field "%s" in install script') % item)
def choose_installer(self):
"""Stage where we choose an install script."""
self.validate_scripts()
base_script = self.installers[0]
self.title_label.set_markup(_("<b>Install %s</b>") % gtk_safe(base_script["name"]))
installer_picker = InstallerPicker(self.installers)
installer_picker.connect("installer-selected", self.on_installer_selected)
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=installer_picker,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
def on_cache_clicked(self, _button):
"""Open the cache configuration dialog"""
CacheConfigurationDialog()
def on_installer_selected(self, _widget, installer_version):
"""Sets the script interpreter to the correct script then proceed to
install folder selection.
If the installed game depends on another one and it's not installed,
prompt the user to install it and quit this installer.
"""
self.clean_widgets()
try:
script = None
for _script in self.installers:
if _script["version"] == installer_version:
script = _script
self.interpreter = interpreter.ScriptInterpreter(script, self)
except MissingGameDependency as ex:
dlg = QuestionDialog(
{
"question": _("This game requires %s. Do you want to install it?") % ex.slug,
"title": _("Missing dependency"),
}
)
if dlg.result == Gtk.ResponseType.YES:
InstallerWindow(
installers=self.installers,
service=self.service,
appid=self.appid,
application=self.application,
)
self.destroy()
return
self.title_label.set_markup(_("<b>Installing {}</b>").format(gtk_safe(self.interpreter.installer.game_name)))
self.select_install_folder()
desktop_shortcut_button = Gtk.CheckButton(_("Create desktop shortcut"), visible=True)
desktop_shortcut_button.connect("clicked", self.on_create_desktop_shortcut_clicked)
self.widget_box.pack_start(desktop_shortcut_button, False, False, 5)
menu_shortcut_button = Gtk.CheckButton(_("Create application menu shortcut"), visible=True)
menu_shortcut_button.connect("clicked", self.on_create_menu_shortcut_clicked)
self.widget_box.pack_start(menu_shortcut_button, False, False, 5)
def select_install_folder(self):
"""Stage where we select the install directory."""
if not self.interpreter.installer.creates_game_folder:
self.on_install_clicked(self.install_button)
return
self.set_message(_("Select installation directory"))
default_path = self.interpreter.get_default_target()
self.set_install_destination(default_path)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_button.hide()
self.source_button.show()
self.install_button.grab_focus()
self.install_button.show()
# self.manual_button.hide()
def on_target_changed(self, text_entry, _data=None):
"""Set the installation target for the game."""
self.interpreter.target_path = os.path.expanduser(text_entry.get_text())
def on_install_clicked(self, button):
"""Let the interpreter take charge of the next stages."""
button.hide()
self.source_button.hide()
self.interpreter.connect("runners-installed", self.on_runners_ready)
GLib.idle_add(self.interpreter.launch_install)
def set_install_destination(self, default_path=None):
"""Display the destination chooser."""
self.install_button.set_visible(False)
self.continue_button.show()
self.continue_button.set_sensitive(False)
location_entry = FileChooserEntry(
"Select folder",
Gtk.FileChooserAction.SELECT_FOLDER,
path=default_path,
warn_if_non_empty=True,
warn_if_ntfs=True
)
location_entry.entry.connect("changed", self.on_target_changed)
self.widget_box.pack_start(location_entry, False, False, 0)
def ask_for_disc(self, message, callback, requires):
"""Ask the user to do insert a CD-ROM."""
self.clean_widgets()
label = InstallerLabel(message)
label.show()
self.widget_box.add(label)
buttons_box = Gtk.Box()
buttons_box.show()
buttons_box.set_margin_top(40)
buttons_box.set_margin_bottom(40)
self.widget_box.add(buttons_box)
autodetect_button = Gtk.Button(label=_("Autodetect"))
autodetect_button.connect("clicked", callback, requires)
autodetect_button.grab_focus()
autodetect_button.show()
buttons_box.pack_start(autodetect_button, True, True, 40)
browse_button = Gtk.Button(label=_("Browse…"))
callback_data = {"callback": callback, "requires": requires}
browse_button.connect("clicked", self.on_browse_clicked, callback_data)
browse_button.show()
buttons_box.pack_start(browse_button, True, True, 40)
def on_browse_clicked(self, widget, callback_data):
dialog = DirectoryDialog(_("Select the folder where the disc is mounted"), parent=self)
folder = dialog.folder
callback = callback_data["callback"]
requires = callback_data["requires"]
callback(widget, requires, folder)
def on_eject_clicked(self, _widget, data=None):
self.interpreter.eject_wine_disc()
def input_menu(self, alias, options, preselect, has_entry, callback):
"""Display an input request as a dropdown menu with options."""
self.clean_widgets()
model = Gtk.ListStore(str, str)
for option in options:
key, label = option.popitem()
model.append([key, label])
combobox = Gtk.ComboBox.new_with_model(model)
renderer_text = Gtk.CellRendererText()
combobox.pack_start(renderer_text, True)
combobox.add_attribute(renderer_text, "text", 1)
combobox.set_id_column(0)
combobox.set_active_id(preselect)
combobox.set_halign(Gtk.Align.CENTER)
self.widget_box.pack_start(combobox, True, False, 100)
combobox.connect("changed", self.on_input_menu_changed)
combobox.show()
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect("clicked", callback, alias, combobox)
self.continue_button.grab_focus()
self.continue_button.show()
self.on_input_menu_changed(combobox)
def on_input_menu_changed(self, widget):
"""Enable continue button if a non-empty choice is selected"""
self.continue_button.set_sensitive(bool(widget.get_active_id()))
def on_runners_ready(self, _widget=None):
"""The runners are ready, proceed with file selection"""
if self.interpreter.extras is None:
extras = self.interpreter.get_extras()
if extras:
self.show_extras(extras)
return
try:
patch_version = self.interpreter.installer.version if self.is_update else None
self.interpreter.installer.prepare_game_files(patch_version)
except UnavailableGame as ex:
raise ScriptingError(str(ex)) from ex
if not self.interpreter.installer.files:
logger.debug("Installer doesn't require files")
self.interpreter.launch_installer_commands()
return
self.show_installer_files_screen()
def show_installer_files_screen(self):
"""Show installer screen with the file picker / downloader"""
self.clean_widgets()
self.set_status(_("Please review the files needed for the installation then click 'Continue'"))
installer_files_box = InstallerFilesBox(self.interpreter.installer, self)
installer_files_box.connect("files-available", self.on_files_available)
installer_files_box.connect("files-ready", self.on_files_ready)
self._cancel_files_func = installer_files_box.stop_all
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=installer_files_box,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
self.continue_button.show()
self.continue_button.set_sensitive(installer_files_box.is_ready)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect(
"clicked", self.on_files_confirmed, installer_files_box
)
def get_extra_label(self, extra):
"""Return a label for the extras picker"""
label = extra["name"]
_infos = []
if extra.get("total_size"):
_infos.append(human_size(extra["total_size"]))
if extra.get("type"):
_infos.append(extra["type"])
if _infos:
label += " (%s)" % ", ".join(_infos)
return label
def show_extras(self, all_extras):
"""Show installer screen with the extras picker"""
self.clean_widgets()
extra_treestore = Gtk.TreeStore(
bool, # is selected?
bool, # is inconsistent?
str, # id
str, # label
)
for extra_source, extras in all_extras.items():
parent = extra_treestore.append(None, (None, None, None, extra_source))
for extra in extras:
extra_treestore.append(parent, (False, False, extra["id"], self.get_extra_label(extra)))
treeview = Gtk.TreeView(extra_treestore)
treeview.set_headers_visible(False)
treeview.expand_all()
renderer_toggle = Gtk.CellRendererToggle()
renderer_toggle.connect("toggled", self.on_extra_toggled, extra_treestore)
renderer_text = Gtk.CellRendererText()
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0, inconsistent=1)
treeview.append_column(installed_column)
label_column = Gtk.TreeViewColumn(None, renderer_text)
label_column.add_attribute(renderer_text, "text", 3)
label_column.set_property("min-width", 80)
treeview.append_column(label_column)
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=treeview,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
scrolledwindow.show_all()
self.widget_box.pack_end(scrolledwindow, True, True, 10)
self.continue_button.show()
self.continue_button.set_sensitive(True)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_treestore)
def on_extra_toggled(self, _widget, path, model):
toggled_row = model[path]
toggled_row_iter = model.get_iter(path)
toggled_row[0] = not toggled_row[0]
toggled_row[1] = False
if model.iter_has_child(toggled_row_iter):
extra_iter = model.iter_children(toggled_row_iter)
while extra_iter:
extra_row = model[extra_iter]
extra_row[0] = toggled_row[0]
extra_iter = model.iter_next(extra_iter)
else:
for heading_row in model:
all_extras_active = True
any_extras_active = False
extra_iter = model.iter_children(heading_row.iter)
while extra_iter:
extra_row = model[extra_iter]
if extra_row[0]:
any_extras_active = True
else:
all_extras_active = False
extra_iter = model.iter_next(extra_iter)
heading_row[0] = all_extras_active
heading_row[1] = any_extras_active
def on_extras_confirmed(self, _button, extra_store):
"""Resume install when user has selected extras to download"""
selected_extras = []
def save_extra(store, path, iter_):
selected, _inconsistent, id_, _label = store[iter_]
if selected and id_:
selected_extras.append(id_)
extra_store.foreach(save_extra)
self.interpreter.extras = selected_extras
GLib.idle_add(self.on_runners_ready)
def on_files_ready(self, _widget, files_ready):
"""Toggle state of continue button based on ready state"""
self.continue_button.set_sensitive(files_ready)
def on_files_confirmed(self, _button, file_box):
"""Call this when the user confirms the install files
This will start the downloads.
"""
self.set_status("")
self.continue_button.set_sensitive(False)
try:
file_box.start_all()
self.continue_button.disconnect(self.continue_handler)
except PermissionError as ex:
self.continue_button.set_sensitive(True)
raise ScriptingError(_("Unable to get files: %s") % ex) from ex
def on_files_available(self, widget):
"""All files are available, continue the install"""
logger.info("All files are available, continuing install")
self._cancel_files_func = None
self.continue_button.hide()
self.interpreter.game_files = widget.get_game_files()
self.clean_widgets()
self.interpreter.launch_installer_commands()
def on_install_finished(self, game_id):
self.clean_widgets()
if self.config.get("create_desktop_shortcut"):
self.create_shortcut(desktop=True)
if self.config.get("create_menu_shortcut"):
self.create_shortcut()
# Save game to trigger a game-updated signal,
# but take care not to create a blank game
if game_id:
game = Game(game_id)
game.save()
self.install_in_progress = False
self.widget_box.show()
self.eject_button.hide()
self.cancel_button.hide()
self.continue_button.hide()
self.install_button.hide()
if game and game.id:
self.play_button.show()
self.close_button.grab_focus()
self.close_button.show()
if not self.is_active():
self.set_urgency_hint(True) # Blink in taskbar
self.connect("focus-in-event", self.on_window_focus)
def on_window_focus(self, _widget, *_args):
"""Remove urgency hint (flashing indicator) when window receives focus"""
self.set_urgency_hint(False)
def on_install_error(self, message):
self.clean_widgets()
self.set_status(message)
self.cancel_button.grab_focus()
def launch_game(self, widget, _data=None):
"""Launch a game after it's been installed."""
widget.set_sensitive(False)
self.on_destroy(widget)
game = Game(self.interpreter.installer.game_id)
if game.id:
game.emit("game-launch")
else:
logger.error("Game has no ID, launch button should not be drawn")
def on_destroy(self, _widget, _data=None):
"""destroy event handler"""
if self.install_in_progress:
if self.confirm_cancel():
return True
else:
if self.interpreter:
self.interpreter.cleanup()
self.destroy()
def on_create_desktop_shortcut_clicked(self, _widget):
self.config["create_desktop_shortcut"] = True
def on_create_menu_shortcut_clicked(self, _widget):
self.config["create_menu_shortcut"] = True
def create_shortcut(self, desktop=False):
"""Create desktop or global menu shortcuts."""
game_slug = self.interpreter.installer.game_slug
game_id = self.interpreter.installer.game_id
game_name = self.interpreter.installer.game_name
if desktop:
xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
else:
xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)
def confirm_cancel(self, _widget=None):
"""Ask a confirmation before cancelling the install"""
remove_checkbox = Gtk.CheckButton.new_with_label(_("Remove game files"))
if self.interpreter and self.interpreter.target_path:
remove_checkbox.set_active(self.interpreter.game_dir_created)
remove_checkbox.show()
confirm_cancel_dialog = QuestionDialog(
{
"question": _("Are you sure you want to cancel the installation?"),
"title": _("Cancel installation?"),
"widgets": [remove_checkbox]
}
)
if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
logger.debug("User aborted installation cancellation")
return True
if self._cancel_files_func:
self._cancel_files_func()
if self.interpreter:
self.interpreter.revert(remove_game_dir=remove_checkbox.get_active())
self.interpreter.cleanup() # still remove temporary downloads in any case
self.destroy()
def on_source_clicked(self, _button):
InstallerSourceDialog(
self.interpreter.installer.script_pretty,
self.interpreter.installer.game_name,
self
)
def clean_widgets(self):
"""Cleanup before displaying the next stage."""
for child_widget in self.widget_box.get_children():
child_widget.destroy()
def set_status(self, text):
"""Display a short status text."""
self.status_label.set_text(text)
def set_message(self, message):
"""Display a message."""
label = InstallerLabel()
label.set_markup("<b>%s</b>" % add_url_tags(message))
label.show()
self.widget_box.pack_start(label, False, False, 18)
def add_spinner(self):
"""Show a spinner in the middle of the view"""
self.clean_widgets()
spinner = Gtk.Spinner()
self.widget_box.pack_start(spinner, False, False, 18)
spinner.show()
spinner.start()
def attach_logger(self, command):
"""Creates a TextBuffer and attach it to a command"""
self.log_buffer = Gtk.TextBuffer()
command.set_log_buffer(self.log_buffer)
self.log_textview = LogTextView(self.log_buffer)
scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
scrolledwindow.show()
self.log_textview.show()
__init__(self, installers, service=None, appid=None, application=None, is_update=False)
special
¶
Source code in lutris/gui/installerwindow.py
def __init__(
self,
installers,
service=None,
appid=None,
application=None,
is_update=False
):
super().__init__(application=application)
self.set_default_size(540, 320)
self.installers = installers
self.config = {}
self.service = service
self.appid = appid
self.install_in_progress = False
self.interpreter = None
self.is_update = is_update
self.log_buffer = None
self.log_textview = None
self._cancel_files_func = None
self.title_label = InstallerLabel()
self.title_label.set_selectable(False)
self.vbox.add(self.title_label)
self.status_label = InstallerLabel()
self.status_label.set_max_width_chars(80)
self.status_label.set_property("wrap", True)
self.status_label.set_selectable(True)
self.vbox.add(self.status_label)
self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.vbox.pack_start(self.widget_box, True, True, 0)
self.vbox.add(Gtk.HSeparator())
button_box = Gtk.Box()
self.cache_button = Gtk.Button(_("Cache"))
self.cache_button.connect("clicked", self.on_cache_clicked)
button_box.add(self.cache_button)
self.action_buttons = Gtk.Box(spacing=6)
action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
action_buttons_alignment.add(self.action_buttons)
button_box.pack_end(action_buttons_alignment, True, True, 0)
self.vbox.pack_start(button_box, False, True, 0)
self.cancel_button = self.add_button(
_("C_ancel"), self.confirm_cancel, tooltip=_("Abort and revert the installation")
)
self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
self.source_button = self.add_button(_("_View source"), self.on_source_clicked)
self.install_button = self.add_button(_("_Install"), self.on_install_clicked)
self.continue_button = self.add_button(_("_Continue"))
self.play_button = self.add_button(_("_Launch"), self.launch_game)
self.close_button = self.add_button(_("_Close"), self.on_destroy)
self.continue_handler = None
self.clean_widgets()
self.show_all()
self.close_button.hide()
self.play_button.hide()
self.install_button.hide()
self.source_button.hide()
self.eject_button.hide()
self.continue_button.hide()
self.install_in_progress = True
self.widget_box.show()
self.title_label.show()
self.choose_installer()
self.present()
add_button(self, label, handler=None, tooltip=None)
¶
Add a button to the action buttons box
Source code in lutris/gui/installerwindow.py
def add_button(self, label, handler=None, tooltip=None):
"""Add a button to the action buttons box"""
button = Gtk.Button.new_with_mnemonic(label)
if tooltip:
button.set_tooltip_text(tooltip)
if handler:
button.connect("clicked", handler)
self.action_buttons.add(button)
return button
add_spinner(self)
¶
Show a spinner in the middle of the view
Source code in lutris/gui/installerwindow.py
def add_spinner(self):
"""Show a spinner in the middle of the view"""
self.clean_widgets()
spinner = Gtk.Spinner()
self.widget_box.pack_start(spinner, False, False, 18)
spinner.show()
spinner.start()
ask_for_disc(self, message, callback, requires)
¶
Ask the user to do insert a CD-ROM.
Source code in lutris/gui/installerwindow.py
def ask_for_disc(self, message, callback, requires):
"""Ask the user to do insert a CD-ROM."""
self.clean_widgets()
label = InstallerLabel(message)
label.show()
self.widget_box.add(label)
buttons_box = Gtk.Box()
buttons_box.show()
buttons_box.set_margin_top(40)
buttons_box.set_margin_bottom(40)
self.widget_box.add(buttons_box)
autodetect_button = Gtk.Button(label=_("Autodetect"))
autodetect_button.connect("clicked", callback, requires)
autodetect_button.grab_focus()
autodetect_button.show()
buttons_box.pack_start(autodetect_button, True, True, 40)
browse_button = Gtk.Button(label=_("Browse…"))
callback_data = {"callback": callback, "requires": requires}
browse_button.connect("clicked", self.on_browse_clicked, callback_data)
browse_button.show()
buttons_box.pack_start(browse_button, True, True, 40)
attach_logger(self, command)
¶
Creates a TextBuffer and attach it to a command
Source code in lutris/gui/installerwindow.py
def attach_logger(self, command):
"""Creates a TextBuffer and attach it to a command"""
self.log_buffer = Gtk.TextBuffer()
command.set_log_buffer(self.log_buffer)
self.log_textview = LogTextView(self.log_buffer)
scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
scrolledwindow.show()
self.log_textview.show()
choose_installer(self)
¶
Stage where we choose an install script.
Source code in lutris/gui/installerwindow.py
def choose_installer(self):
"""Stage where we choose an install script."""
self.validate_scripts()
base_script = self.installers[0]
self.title_label.set_markup(_("<b>Install %s</b>") % gtk_safe(base_script["name"]))
installer_picker = InstallerPicker(self.installers)
installer_picker.connect("installer-selected", self.on_installer_selected)
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=installer_picker,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
clean_widgets(self)
¶
Cleanup before displaying the next stage.
Source code in lutris/gui/installerwindow.py
def clean_widgets(self):
"""Cleanup before displaying the next stage."""
for child_widget in self.widget_box.get_children():
child_widget.destroy()
confirm_cancel(self, _widget=None)
¶
Ask a confirmation before cancelling the install
Source code in lutris/gui/installerwindow.py
def confirm_cancel(self, _widget=None):
"""Ask a confirmation before cancelling the install"""
remove_checkbox = Gtk.CheckButton.new_with_label(_("Remove game files"))
if self.interpreter and self.interpreter.target_path:
remove_checkbox.set_active(self.interpreter.game_dir_created)
remove_checkbox.show()
confirm_cancel_dialog = QuestionDialog(
{
"question": _("Are you sure you want to cancel the installation?"),
"title": _("Cancel installation?"),
"widgets": [remove_checkbox]
}
)
if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
logger.debug("User aborted installation cancellation")
return True
if self._cancel_files_func:
self._cancel_files_func()
if self.interpreter:
self.interpreter.revert(remove_game_dir=remove_checkbox.get_active())
self.interpreter.cleanup() # still remove temporary downloads in any case
self.destroy()
create_shortcut(self, desktop=False)
¶
Create desktop or global menu shortcuts.
Source code in lutris/gui/installerwindow.py
def create_shortcut(self, desktop=False):
"""Create desktop or global menu shortcuts."""
game_slug = self.interpreter.installer.game_slug
game_id = self.interpreter.installer.game_id
game_name = self.interpreter.installer.game_name
if desktop:
xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
else:
xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)
get_extra_label(self, extra)
¶
Return a label for the extras picker
Source code in lutris/gui/installerwindow.py
def get_extra_label(self, extra):
"""Return a label for the extras picker"""
label = extra["name"]
_infos = []
if extra.get("total_size"):
_infos.append(human_size(extra["total_size"]))
if extra.get("type"):
_infos.append(extra["type"])
if _infos:
label += " (%s)" % ", ".join(_infos)
return label
input_menu(self, alias, options, preselect, has_entry, callback)
¶
Display an input request as a dropdown menu with options.
Source code in lutris/gui/installerwindow.py
def input_menu(self, alias, options, preselect, has_entry, callback):
"""Display an input request as a dropdown menu with options."""
self.clean_widgets()
model = Gtk.ListStore(str, str)
for option in options:
key, label = option.popitem()
model.append([key, label])
combobox = Gtk.ComboBox.new_with_model(model)
renderer_text = Gtk.CellRendererText()
combobox.pack_start(renderer_text, True)
combobox.add_attribute(renderer_text, "text", 1)
combobox.set_id_column(0)
combobox.set_active_id(preselect)
combobox.set_halign(Gtk.Align.CENTER)
self.widget_box.pack_start(combobox, True, False, 100)
combobox.connect("changed", self.on_input_menu_changed)
combobox.show()
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect("clicked", callback, alias, combobox)
self.continue_button.grab_focus()
self.continue_button.show()
self.on_input_menu_changed(combobox)
launch_game(self, widget, _data=None)
¶
Launch a game after it's been installed.
Source code in lutris/gui/installerwindow.py
def launch_game(self, widget, _data=None):
"""Launch a game after it's been installed."""
widget.set_sensitive(False)
self.on_destroy(widget)
game = Game(self.interpreter.installer.game_id)
if game.id:
game.emit("game-launch")
else:
logger.error("Game has no ID, launch button should not be drawn")
on_browse_clicked(self, widget, callback_data)
¶
Source code in lutris/gui/installerwindow.py
def on_browse_clicked(self, widget, callback_data):
dialog = DirectoryDialog(_("Select the folder where the disc is mounted"), parent=self)
folder = dialog.folder
callback = callback_data["callback"]
requires = callback_data["requires"]
callback(widget, requires, folder)
on_cache_clicked(self, _button)
¶
Open the cache configuration dialog
Source code in lutris/gui/installerwindow.py
def on_cache_clicked(self, _button):
"""Open the cache configuration dialog"""
CacheConfigurationDialog()
on_create_desktop_shortcut_clicked(self, _widget)
¶
Source code in lutris/gui/installerwindow.py
def on_create_desktop_shortcut_clicked(self, _widget):
self.config["create_desktop_shortcut"] = True
on_create_menu_shortcut_clicked(self, _widget)
¶
Source code in lutris/gui/installerwindow.py
def on_create_menu_shortcut_clicked(self, _widget):
self.config["create_menu_shortcut"] = True
on_destroy(self, _widget, _data=None)
¶
destroy event handler
Source code in lutris/gui/installerwindow.py
def on_destroy(self, _widget, _data=None):
"""destroy event handler"""
if self.install_in_progress:
if self.confirm_cancel():
return True
else:
if self.interpreter:
self.interpreter.cleanup()
self.destroy()
on_eject_clicked(self, _widget, data=None)
¶
Source code in lutris/gui/installerwindow.py
def on_eject_clicked(self, _widget, data=None):
self.interpreter.eject_wine_disc()
on_extra_toggled(self, _widget, path, model)
¶
Source code in lutris/gui/installerwindow.py
def on_extra_toggled(self, _widget, path, model):
toggled_row = model[path]
toggled_row_iter = model.get_iter(path)
toggled_row[0] = not toggled_row[0]
toggled_row[1] = False
if model.iter_has_child(toggled_row_iter):
extra_iter = model.iter_children(toggled_row_iter)
while extra_iter:
extra_row = model[extra_iter]
extra_row[0] = toggled_row[0]
extra_iter = model.iter_next(extra_iter)
else:
for heading_row in model:
all_extras_active = True
any_extras_active = False
extra_iter = model.iter_children(heading_row.iter)
while extra_iter:
extra_row = model[extra_iter]
if extra_row[0]:
any_extras_active = True
else:
all_extras_active = False
extra_iter = model.iter_next(extra_iter)
heading_row[0] = all_extras_active
heading_row[1] = any_extras_active
on_extras_confirmed(self, _button, extra_store)
¶
Resume install when user has selected extras to download
Source code in lutris/gui/installerwindow.py
def on_extras_confirmed(self, _button, extra_store):
"""Resume install when user has selected extras to download"""
selected_extras = []
def save_extra(store, path, iter_):
selected, _inconsistent, id_, _label = store[iter_]
if selected and id_:
selected_extras.append(id_)
extra_store.foreach(save_extra)
self.interpreter.extras = selected_extras
GLib.idle_add(self.on_runners_ready)
on_files_available(self, widget)
¶
All files are available, continue the install
Source code in lutris/gui/installerwindow.py
def on_files_available(self, widget):
"""All files are available, continue the install"""
logger.info("All files are available, continuing install")
self._cancel_files_func = None
self.continue_button.hide()
self.interpreter.game_files = widget.get_game_files()
self.clean_widgets()
self.interpreter.launch_installer_commands()
on_files_confirmed(self, _button, file_box)
¶
Call this when the user confirms the install files This will start the downloads.
Source code in lutris/gui/installerwindow.py
def on_files_confirmed(self, _button, file_box):
"""Call this when the user confirms the install files
This will start the downloads.
"""
self.set_status("")
self.continue_button.set_sensitive(False)
try:
file_box.start_all()
self.continue_button.disconnect(self.continue_handler)
except PermissionError as ex:
self.continue_button.set_sensitive(True)
raise ScriptingError(_("Unable to get files: %s") % ex) from ex
on_files_ready(self, _widget, files_ready)
¶
Toggle state of continue button based on ready state
Source code in lutris/gui/installerwindow.py
def on_files_ready(self, _widget, files_ready):
"""Toggle state of continue button based on ready state"""
self.continue_button.set_sensitive(files_ready)
on_input_menu_changed(self, widget)
¶
Enable continue button if a non-empty choice is selected
Source code in lutris/gui/installerwindow.py
def on_input_menu_changed(self, widget):
"""Enable continue button if a non-empty choice is selected"""
self.continue_button.set_sensitive(bool(widget.get_active_id()))
on_install_clicked(self, button)
¶
Let the interpreter take charge of the next stages.
Source code in lutris/gui/installerwindow.py
def on_install_clicked(self, button):
"""Let the interpreter take charge of the next stages."""
button.hide()
self.source_button.hide()
self.interpreter.connect("runners-installed", self.on_runners_ready)
GLib.idle_add(self.interpreter.launch_install)
on_install_error(self, message)
¶
Source code in lutris/gui/installerwindow.py
def on_install_error(self, message):
self.clean_widgets()
self.set_status(message)
self.cancel_button.grab_focus()
on_install_finished(self, game_id)
¶
Source code in lutris/gui/installerwindow.py
def on_install_finished(self, game_id):
self.clean_widgets()
if self.config.get("create_desktop_shortcut"):
self.create_shortcut(desktop=True)
if self.config.get("create_menu_shortcut"):
self.create_shortcut()
# Save game to trigger a game-updated signal,
# but take care not to create a blank game
if game_id:
game = Game(game_id)
game.save()
self.install_in_progress = False
self.widget_box.show()
self.eject_button.hide()
self.cancel_button.hide()
self.continue_button.hide()
self.install_button.hide()
if game and game.id:
self.play_button.show()
self.close_button.grab_focus()
self.close_button.show()
if not self.is_active():
self.set_urgency_hint(True) # Blink in taskbar
self.connect("focus-in-event", self.on_window_focus)
on_installer_selected(self, _widget, installer_version)
¶
Sets the script interpreter to the correct script then proceed to install folder selection.
If the installed game depends on another one and it's not installed, prompt the user to install it and quit this installer.
Source code in lutris/gui/installerwindow.py
def on_installer_selected(self, _widget, installer_version):
"""Sets the script interpreter to the correct script then proceed to
install folder selection.
If the installed game depends on another one and it's not installed,
prompt the user to install it and quit this installer.
"""
self.clean_widgets()
try:
script = None
for _script in self.installers:
if _script["version"] == installer_version:
script = _script
self.interpreter = interpreter.ScriptInterpreter(script, self)
except MissingGameDependency as ex:
dlg = QuestionDialog(
{
"question": _("This game requires %s. Do you want to install it?") % ex.slug,
"title": _("Missing dependency"),
}
)
if dlg.result == Gtk.ResponseType.YES:
InstallerWindow(
installers=self.installers,
service=self.service,
appid=self.appid,
application=self.application,
)
self.destroy()
return
self.title_label.set_markup(_("<b>Installing {}</b>").format(gtk_safe(self.interpreter.installer.game_name)))
self.select_install_folder()
desktop_shortcut_button = Gtk.CheckButton(_("Create desktop shortcut"), visible=True)
desktop_shortcut_button.connect("clicked", self.on_create_desktop_shortcut_clicked)
self.widget_box.pack_start(desktop_shortcut_button, False, False, 5)
menu_shortcut_button = Gtk.CheckButton(_("Create application menu shortcut"), visible=True)
menu_shortcut_button.connect("clicked", self.on_create_menu_shortcut_clicked)
self.widget_box.pack_start(menu_shortcut_button, False, False, 5)
on_runners_ready(self, _widget=None)
¶
The runners are ready, proceed with file selection
Source code in lutris/gui/installerwindow.py
def on_runners_ready(self, _widget=None):
"""The runners are ready, proceed with file selection"""
if self.interpreter.extras is None:
extras = self.interpreter.get_extras()
if extras:
self.show_extras(extras)
return
try:
patch_version = self.interpreter.installer.version if self.is_update else None
self.interpreter.installer.prepare_game_files(patch_version)
except UnavailableGame as ex:
raise ScriptingError(str(ex)) from ex
if not self.interpreter.installer.files:
logger.debug("Installer doesn't require files")
self.interpreter.launch_installer_commands()
return
self.show_installer_files_screen()
on_source_clicked(self, _button)
¶
Source code in lutris/gui/installerwindow.py
def on_source_clicked(self, _button):
InstallerSourceDialog(
self.interpreter.installer.script_pretty,
self.interpreter.installer.game_name,
self
)
on_target_changed(self, text_entry, _data=None)
¶
Set the installation target for the game.
Source code in lutris/gui/installerwindow.py
def on_target_changed(self, text_entry, _data=None):
"""Set the installation target for the game."""
self.interpreter.target_path = os.path.expanduser(text_entry.get_text())
on_window_focus(self, _widget, *_args)
¶
Remove urgency hint (flashing indicator) when window receives focus
Source code in lutris/gui/installerwindow.py
def on_window_focus(self, _widget, *_args):
"""Remove urgency hint (flashing indicator) when window receives focus"""
self.set_urgency_hint(False)
select_install_folder(self)
¶
Stage where we select the install directory.
Source code in lutris/gui/installerwindow.py
def select_install_folder(self):
"""Stage where we select the install directory."""
if not self.interpreter.installer.creates_game_folder:
self.on_install_clicked(self.install_button)
return
self.set_message(_("Select installation directory"))
default_path = self.interpreter.get_default_target()
self.set_install_destination(default_path)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_button.hide()
self.source_button.show()
self.install_button.grab_focus()
self.install_button.show()
# self.manual_button.hide()
set_install_destination(self, default_path=None)
¶
Display the destination chooser.
Source code in lutris/gui/installerwindow.py
def set_install_destination(self, default_path=None):
"""Display the destination chooser."""
self.install_button.set_visible(False)
self.continue_button.show()
self.continue_button.set_sensitive(False)
location_entry = FileChooserEntry(
"Select folder",
Gtk.FileChooserAction.SELECT_FOLDER,
path=default_path,
warn_if_non_empty=True,
warn_if_ntfs=True
)
location_entry.entry.connect("changed", self.on_target_changed)
self.widget_box.pack_start(location_entry, False, False, 0)
set_message(self, message)
¶
Display a message.
Source code in lutris/gui/installerwindow.py
def set_message(self, message):
"""Display a message."""
label = InstallerLabel()
label.set_markup("<b>%s</b>" % add_url_tags(message))
label.show()
self.widget_box.pack_start(label, False, False, 18)
set_status(self, text)
¶
Display a short status text.
Source code in lutris/gui/installerwindow.py
def set_status(self, text):
"""Display a short status text."""
self.status_label.set_text(text)
show_extras(self, all_extras)
¶
Show installer screen with the extras picker
Source code in lutris/gui/installerwindow.py
def show_extras(self, all_extras):
"""Show installer screen with the extras picker"""
self.clean_widgets()
extra_treestore = Gtk.TreeStore(
bool, # is selected?
bool, # is inconsistent?
str, # id
str, # label
)
for extra_source, extras in all_extras.items():
parent = extra_treestore.append(None, (None, None, None, extra_source))
for extra in extras:
extra_treestore.append(parent, (False, False, extra["id"], self.get_extra_label(extra)))
treeview = Gtk.TreeView(extra_treestore)
treeview.set_headers_visible(False)
treeview.expand_all()
renderer_toggle = Gtk.CellRendererToggle()
renderer_toggle.connect("toggled", self.on_extra_toggled, extra_treestore)
renderer_text = Gtk.CellRendererText()
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0, inconsistent=1)
treeview.append_column(installed_column)
label_column = Gtk.TreeViewColumn(None, renderer_text)
label_column.add_attribute(renderer_text, "text", 3)
label_column.set_property("min-width", 80)
treeview.append_column(label_column)
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=treeview,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
scrolledwindow.show_all()
self.widget_box.pack_end(scrolledwindow, True, True, 10)
self.continue_button.show()
self.continue_button.set_sensitive(True)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_treestore)
show_installer_files_screen(self)
¶
Show installer screen with the file picker / downloader
Source code in lutris/gui/installerwindow.py
def show_installer_files_screen(self):
"""Show installer screen with the file picker / downloader"""
self.clean_widgets()
self.set_status(_("Please review the files needed for the installation then click 'Continue'"))
installer_files_box = InstallerFilesBox(self.interpreter.installer, self)
installer_files_box.connect("files-available", self.on_files_available)
installer_files_box.connect("files-ready", self.on_files_ready)
self._cancel_files_func = installer_files_box.stop_all
scrolledwindow = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
child=installer_files_box,
visible=True
)
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
self.widget_box.pack_end(scrolledwindow, True, True, 10)
self.continue_button.show()
self.continue_button.set_sensitive(installer_files_box.is_ready)
if self.continue_handler:
self.continue_button.disconnect(self.continue_handler)
self.continue_handler = self.continue_button.connect(
"clicked", self.on_files_confirmed, installer_files_box
)
validate_scripts(self)
¶
Auto-fixes some script aspects and checks for mandatory fields
Source code in lutris/gui/installerwindow.py
def validate_scripts(self):
"""Auto-fixes some script aspects and checks for mandatory fields"""
if not self.installers:
raise ScriptingError("No installer available")
for script in self.installers:
for item in ["description", "notes"]:
script[item] = script.get(item) or ""
for item in ["name", "runner", "version"]:
if item not in script:
logger.error("Invalid script: %s", script)
raise ScriptingError(_('Missing field "%s" in install script') % item)
lutriswindow
¶
Main window for the Lutris interface.
LutrisWindow (ApplicationWindow)
¶
Handler class for main window signals.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate(ui=os.path.join(datapath.get(), "ui", "lutris-window.ui"))
class LutrisWindow(Gtk.ApplicationWindow): # pylint: disable=too-many-public-methods
"""Handler class for main window signals."""
default_view_type = "grid"
default_width = 800
default_height = 600
__gtype_name__ = "LutrisWindow"
__gsignals__ = {
"view-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
games_scrollwindow = GtkTemplate.Child()
sidebar_revealer = GtkTemplate.Child()
sidebar_scrolled = GtkTemplate.Child()
game_revealer = GtkTemplate.Child()
search_entry = GtkTemplate.Child()
zoom_adjustment = GtkTemplate.Child()
blank_overlay = GtkTemplate.Child()
viewtype_icon = GtkTemplate.Child()
def __init__(self, application, **kwargs):
width = int(settings.read_setting("width") or self.default_width)
height = int(settings.read_setting("height") or self.default_height)
super().__init__(
default_width=width,
default_height=height,
window_position=Gtk.WindowPosition.NONE,
name="lutris",
icon_name="lutris",
application=application,
**kwargs
)
update_desktop_icons()
load_icon_theme()
self.application = application
self.window_x = settings.read_setting("window_x")
self.window_y = settings.read_setting("window_y")
if self.window_x and self.window_y:
self.move(int(self.window_x), int(self.window_y))
self.threads_stoppers = []
self.window_size = (width, height)
self.maximized = settings.read_setting("maximized") == "True"
self.service = None
self.game_actions = GameActions(application=application, window=self)
self.search_timer_id = None
self.selected_category = settings.read_setting("selected_category", default="runner:all")
self.filters = self.load_filters()
self.set_service(self.filters.get("service"))
self.icon_type = self.load_icon_type()
self.game_store = GameStore(self.service, self.service_media)
self.view = Gtk.Box()
self.connect("delete-event", self.on_window_delete)
self.connect("configure-event", self.on_window_configure)
self.connect("realize", self.on_load)
if self.maximized:
self.maximize()
self.init_template()
self._init_actions()
self.set_viewtype_icon(self.view_type)
lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
lutris_icon.set_margin_right(3)
self.sidebar = LutrisSidebar(self.application, selected=self.selected_category)
self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
# "realize" is order sensitive- must connect after sidebar itself connects the same signal
self.sidebar.connect("realize", self.on_sidebar_realize)
self.sidebar_scrolled.add(self.sidebar)
# This must wait until the selected-rows-changed signal is connected
self.sidebar.initialize_rows()
self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
self.sidebar_revealer.set_transition_duration(300)
self.game_bar = None
self.revealer_box = Gtk.HBox(visible=True)
self.game_revealer.add(self.revealer_box)
self.connect("view-updated", self.update_store)
GObject.add_emission_hook(BaseService, "service-login", self.on_service_login)
GObject.add_emission_hook(BaseService, "service-logout", self.on_service_logout)
GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
GObject.add_emission_hook(Game, "game-stopped", self.on_game_stopped)
GObject.add_emission_hook(Game, "game-removed", self.on_game_collection_changed)
def _init_actions(self):
Action = namedtuple("Action", ("callback", "type", "enabled", "default", "accel"))
Action.__new__.__defaults__ = (None, None, True, None, None)
actions = {
"add-game": Action(self.on_add_game_button_clicked),
"preferences": Action(self.on_preferences_activate),
"about": Action(self.on_about_clicked),
"show-installed-only": Action( # delete?
self.on_show_installed_state_change,
type="b",
default=self.filter_installed,
accel="<Primary>h",
),
"toggle-viewtype": Action(self.on_toggle_viewtype),
"icon-type": Action(self.on_icontype_state_change, type="s", default=self.icon_type),
"view-sorting": Action(self.on_view_sorting_state_change, type="s", default=self.view_sorting),
"view-sorting-ascending": Action(
self.on_view_sorting_direction_change,
type="b",
default=self.view_sorting_ascending,
),
"show-side-panel": Action(
self.on_side_panel_state_change,
type="b",
default=self.side_panel_visible,
accel="F9",
),
"show-hidden-games": Action(
self.hidden_state_change,
type="b",
default=self.show_hidden_games,
),
"open-forums": Action(lambda *x: open_uri("https://forums.lutris.net/")),
"open-discord": Action(lambda *x: open_uri("https://discord.gg/Pnt5CuY")),
"donate": Action(lambda *x: open_uri("https://lutris.net/donate")),
}
self.actions = {}
app = self.props.application
for name, value in actions.items():
if not value.type:
action = Gio.SimpleAction.new(name)
action.connect("activate", value.callback)
else:
default_value = None
param_type = None
if value.default is not None:
default_value = GLib.Variant(value.type, value.default)
if value.type != "b":
param_type = default_value.get_type()
action = Gio.SimpleAction.new_stateful(name, param_type, default_value)
action.connect("change-state", value.callback)
self.actions[name] = action
if value.enabled is False:
action.props.enabled = False
self.add_action(action)
if value.accel:
app.add_accelerator(value.accel, "win." + name)
@property
def service_media(self):
return self.get_service_media(self.load_icon_type())
def on_load(self, widget, data=None):
"""Finish initializing the view"""
self._bind_zoom_adjustment()
self.view.grab_focus()
self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
def on_sidebar_realize(self, widget, data=None):
"""Grab the initial focus after the sidebar is initialized - so the view is ready."""
self.view.grab_focus()
def load_filters(self):
"""Load the initial filters when creating the view"""
category, value = self.selected_category.split(":")
filters = {
category: value
} # Type of filter corresponding to the selected sidebar element
filters["hidden"] = settings.read_setting("show_hidden_games").lower() == "true"
filters["installed"] = settings.read_setting("filter_installed").lower() == "true"
return filters
def hidden_state_change(self, action, value):
"""Hides or shows the hidden games"""
action.set_state(value)
settings.write_setting("show_hidden_games", str(value).lower(), section="lutris")
self.filters["hidden"] = value
self.emit("view-updated")
@property
def current_view_type(self):
"""Returns which kind of view is currently presented (grid or list)"""
return settings.read_setting("view_type") or "grid"
@property
def filter_installed(self):
return settings.read_setting("filter_installed").lower() == "true"
@property
def side_panel_visible(self):
return settings.read_setting("side_panel_visible").lower() != "false"
@property
def show_tray_icon(self):
"""Setting to hide or show status icon"""
return settings.read_setting("show_tray_icon", default="false").lower() == "true"
@property
def view_sorting(self):
value = settings.read_setting("view_sorting") or "name"
if value.endswith("_text"):
value = value[:-5]
return value
@property
def view_sorting_ascending(self):
return settings.read_setting("view_sorting_ascending").lower() != "false"
@property
def show_hidden_games(self):
return settings.read_setting("show_hidden_games").lower() == "true"
@property
def sort_params(self):
_sort_params = [("installed", "COLLATE NOCASE DESC")]
_sort_params.append((
self.view_sorting,
"COLLATE NOCASE ASC"
if self.view_sorting_ascending
else "COLLATE NOCASE DESC"
))
return _sort_params
def get_running_games(self):
"""Return a list of currently running games"""
return games_db.get_games_by_ids([game.id for game in self.application.running_games])
def get_recent_games(self):
"""Return a list of currently running games"""
searches, _filters, excludes = self.get_sql_filters()
games = games_db.get_games(searches=searches, filters={'installed': '1'}, excludes=excludes)
return sorted(
games,
key=lambda game: max(game["installed_at"] or 0, game["lastplayed"] or 0),
reverse=True
)
def game_matches(self, game):
if self.filters.get("installed"):
if game["appid"] not in games_db.get_service_games(self.service.id):
return False
if not self.filters.get("text"):
return True
return self.filters["text"] in game["name"].lower()
def set_service(self, service_name):
if self.service and self.service.id == service_name:
return self.service
if not service_name:
self.service = None
return
try:
self.service = services.SERVICES[service_name]()
except KeyError:
logger.error("Non existent service '%s'", service_name)
self.service = None
return self.service
@staticmethod
def combine_games(service_game, lutris_game):
"""Inject lutris game information into a service game"""
if lutris_game and service_game["appid"] == lutris_game["service_id"]:
for field in ("platform", "runner", "year", "installed_at", "lastplayed", "playtime", "installed"):
service_game[field] = lutris_game[field]
return service_game
def get_service_games(self, service_name):
"""Switch the current service to service_name and return games if available"""
service_games = ServiceGameCollection.get_for_service(service_name)
if service_name == "lutris":
lutris_games = {g["slug"]: g for g in games_db.get_games()}
else:
lutris_games = {g["service_id"]: g for g in games_db.get_games(filters={"service": self.service.id})}
def get_sort_value(game):
sort_defaults = {
"name": "",
"year": 0,
"lastplayed": 0.0,
"installed_at": 0.0,
"playtime": 0.0,
}
view_sorting = self.view_sorting
lutris_game = lutris_games.get(game["appid"])
if not lutris_game:
return sort_defaults.get(view_sorting, "")
value = lutris_game.get(view_sorting)
if value:
return value
# Users may have obsolete view_sorting settings, so
# we must tolerate them. We treat them all as blank.
return sort_defaults.get(view_sorting, "")
return [
self.combine_games(game, lutris_games.get(game["appid"])) for game in sorted(
service_games,
key=get_sort_value,
reverse=not self.view_sorting_ascending
) if self.game_matches(game)
]
def get_games_from_filters(self):
service_name = self.filters.get("service")
if service_name in services.SERVICES:
if self.service.online and not self.service.is_authenticated():
self.show_label(_("Connect your %s account to access your games") % self.service.name)
return []
return self.get_service_games(service_name)
dynamic_categories = {
"recent": self.get_recent_games,
"running": self.get_running_games,
}
if self.filters.get("dynamic_category") in dynamic_categories:
return dynamic_categories[self.filters["dynamic_category"]]()
if self.filters.get("category") and self.filters["category"] != "all":
game_ids = categories_db.get_game_ids_for_category(self.filters["category"])
else:
game_ids = None
searches, filters, excludes = self.get_sql_filters()
games = games_db.get_games(
searches=searches,
filters=filters,
excludes=excludes,
sorts=self.sort_params
)
if game_ids is not None:
return [game for game in games if game["id"] in game_ids]
return games
def get_sql_filters(self):
"""Return the current filters for the view"""
sql_filters = {}
sql_excludes = {}
if self.filters.get("runner"):
sql_filters["runner"] = self.filters["runner"]
if self.filters.get("platform"):
sql_filters["platform"] = self.filters["platform"]
if self.filters.get("installed"):
sql_filters["installed"] = "1"
if self.filters.get("text"):
searches = {"name": self.filters["text"]}
else:
searches = None
if not self.filters.get("hidden"):
sql_excludes["hidden"] = 1
return searches, sql_filters, sql_excludes
def get_service_media(self, icon_type):
"""Return the ServiceMedia class used for this view"""
service = self.service if self.service else LutrisService
medias = service.medias
if icon_type in medias:
return medias[icon_type]()
return medias[service.default_format]()
def update_revealer(self, game=None):
if game:
if self.game_bar:
self.game_bar.destroy()
self.game_bar = GameBar(game, self.game_actions, self.application)
self.revealer_box.pack_start(self.game_bar, True, True, 0)
elif self.game_bar:
# The game bar can't be destroyed here because the game gets unselected on Wayland
# whenever the game bar is interacted with. Instead, we keep the current game bar open
# when the game gets unselected, which is somewhat closer to what the intended behavior
# should be anyway. Might require closing the game bar manually in some cases.
pass
# self.game_bar.destroy()
if self.revealer_box.get_children():
self.game_revealer.set_reveal_child(True)
else:
self.game_revealer.set_reveal_child(False)
def show_empty_label(self):
"""Display a label when the view is empty"""
if self.filters.get("text"):
self.show_label(_("No games matching '%s' found ") % self.filters["text"])
else:
if self.filters.get("category") == "favorite":
self.show_label(_("Add games to your favorites to see them here."))
elif self.filters.get("installed"):
self.show_label(_("No installed games found. Press Ctrl+H so show all games."))
else:
self.show_splash()
# self.show_label(_("No games found"))
def update_store(self, *_args, **_kwargs):
self.game_store.store.clear()
for child in self.blank_overlay.get_children():
child.destroy()
games = self.get_games_from_filters()
logger.debug("Showing %d games", len(games))
self.view.service = self.service.id if self.service else None
GLib.idle_add(self.update_revealer)
for game in games:
self.game_store.add_game(game)
if not games:
self.show_empty_label()
self.search_timer_id = None
return False
def _bind_zoom_adjustment(self):
"""Bind the zoom slider to the supported banner sizes"""
service = self.service if self.service else LutrisService
media_services = list(service.medias.keys())
self.load_icon_type()
self.zoom_adjustment.set_lower(0)
self.zoom_adjustment.set_upper(len(media_services) - 1)
if self.icon_type in media_services:
value = media_services.index(self.icon_type)
else:
value = 0
self.zoom_adjustment.props.value = value
self.zoom_adjustment.connect("value-changed", self.on_zoom_changed)
def on_zoom_changed(self, adjustment):
"""Handler for zoom modification"""
media_index = round(adjustment.props.value)
adjustment.props.value = media_index
service = self.service if self.service else LutrisService
media_services = list(service.medias.keys())
if len(media_services) <= media_index:
media_index = media_services.index(service.default_format)
icon_type = media_services[media_index]
if icon_type != self.icon_type:
self.save_icon_type(icon_type)
self.show_spinner()
def show_overlay(self, widget, halign=Gtk.Align.FILL, valign=Gtk.Align.FILL):
"""Display a widget in the blank overlay"""
for child in self.blank_overlay.get_children():
child.destroy()
self.blank_overlay.set_halign(halign)
self.blank_overlay.set_valign(valign)
self.blank_overlay.add(widget)
self.blank_overlay.props.visible = True
def show_label(self, message):
"""Display a label in the middle of the UI"""
self.show_overlay(Gtk.Label(message, visible=True))
def show_splash(self):
image = Gtk.Image(visible=True)
image.set_from_file(os.path.join(datapath.get(), "media/splash.svg"))
self.show_overlay(image, Gtk.Align.START, Gtk.Align.START)
def show_spinner(self):
spinner = Gtk.Spinner(visible=True)
spinner.start()
for child in self.blank_overlay.get_children():
child.destroy()
self.blank_overlay.add(spinner)
self.blank_overlay.props.visible = True
def hide_overlay(self):
self.blank_overlay.props.visible = False
for child in self.blank_overlay.get_children():
child.destroy()
@property
def view_type(self):
"""Return the type of view saved by the user"""
view_type = settings.read_setting("view_type")
if view_type in ["grid", "list"]:
return view_type
return self.default_view_type
def do_key_press_event(self, event): # pylint: disable=arguments-differ
# XXX: This block of code below is to enable searching on type.
# Enabling this feature steals focus from other entries so it needs
# some kind of focus detection before enabling library search.
# Probably not ideal for non-english, but we want to limit
# which keys actually start searching
if event.keyval == Gdk.KEY_Escape:
self.search_entry.set_text("")
self.view.grab_focus()
return Gtk.ApplicationWindow.do_key_press_event(self, event)
if ( # pylint: disable=too-many-boolean-expressions
not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z or event.state & Gdk.ModifierType.CONTROL_MASK
or event.state & Gdk.ModifierType.SHIFT_MASK or event.state & Gdk.ModifierType.META_MASK
or event.state & Gdk.ModifierType.MOD1_MASK or self.search_entry.has_focus()
):
return Gtk.ApplicationWindow.do_key_press_event(self, event)
self.search_entry.grab_focus()
return self.search_entry.do_key_press_event(self.search_entry, event)
def load_icon_type(self):
"""Return the icon style depending on the type of view."""
setting_key = "icon_type_%sview" % self.current_view_type
if self.service and self.service.id != "lutris":
setting_key += "_%s" % self.service.id
self.icon_type = settings.read_setting(setting_key)
return self.icon_type
def save_icon_type(self, icon_type):
"""Save icon type to settings"""
self.icon_type = icon_type
setting_key = "icon_type_%sview" % self.current_view_type
if self.service and self.service.id != "lutris":
setting_key += "_%s" % self.service.id
settings.write_setting(setting_key, self.icon_type)
self.redraw_view()
def redraw_view(self):
"""Completely reconstruct the main view"""
if not self.game_store:
logger.error("No game store yet")
return
if self.view:
self.view.destroy()
self.game_store = GameStore(self.service, self.service_media)
if self.view_type == "grid":
self.view = GameGridView(
self.game_store,
self.game_store.service_media,
hide_text=settings.read_setting("hide_text_under_icons") == "True"
)
else:
self.view = GameListView(self.game_store, self.game_store.service_media)
self.view.connect("game-selected", self.on_game_selection_changed)
self.view.connect("game-activated", self.on_game_activated)
self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
for child in self.games_scrollwindow.get_children():
child.destroy()
self.games_scrollwindow.add(self.view)
self.view.show_all()
self.update_store()
def set_viewtype_icon(self, view_type):
self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type, Gtk.IconSize.BUTTON)
def set_show_installed_state(self, filter_installed):
"""Shows or hide uninstalled games"""
settings.write_setting("filter_installed", bool(filter_installed))
self.filters["installed"] = filter_installed
def on_service_games_updated(self, service):
"""Request a view update when service games are loaded"""
if self.service and service.id == self.service.id:
self.emit("view-updated")
return True
def on_service_login(self, service):
AsyncCall(service.reload, None)
return True
def on_service_logout(self, service):
if self.service and service.id == self.service.id:
self.emit("view-updated")
return True
@GtkTemplate.Callback
def on_resize(self, widget, *_args):
"""Size-allocate signal.
Updates stored window size and maximized state.
"""
if not widget.get_window():
return
self.maximized = widget.is_maximized()
size = widget.get_size()
if not self.maximized:
self.window_size = size
self.search_entry.set_size_request(min(max(50, size[0] - 470), 800), -1)
def on_window_delete(self, *_args):
if self.application.running_games.get_n_items():
self.hide()
return True
def on_window_configure(self, *_args):
"""Callback triggered when the window is moved, resized..."""
self.window_x, self.window_y = self.get_position()
@GtkTemplate.Callback
def on_destroy(self, *_args):
"""Signal for window close."""
# Stop cancellable running threads
for stopper in self.threads_stoppers:
stopper()
# Save settings
width, height = self.window_size
settings.write_setting("width", width)
settings.write_setting("height", height)
if self.window_x and self.window_y:
settings.write_setting("window_x", self.window_x)
settings.write_setting("window_y", self.window_y)
settings.write_setting("maximized", self.maximized)
@GtkTemplate.Callback
def on_preferences_activate(self, *_args):
"""Callback when preferences is activated."""
self.application.show_window(PreferencesDialog)
def on_show_installed_state_change(self, action, value):
"""Callback to handle uninstalled game filter switch"""
action.set_state(value)
self.set_show_installed_state(value.get_boolean())
self.emit("view-updated")
@GtkTemplate.Callback
def on_search_entry_changed(self, entry):
"""Callback for the search input keypresses"""
if self.search_timer_id:
GLib.source_remove(self.search_timer_id)
self.filters["text"] = entry.get_text().lower().strip()
self.search_timer_id = GLib.timeout_add(150, self.update_store)
@GtkTemplate.Callback
def on_search_entry_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Down:
if self.current_view_type == 'grid':
self.view.select_path(Gtk.TreePath('0')) # needed for gridview only
# if game_bar is alive at this point it can mess grid item selection up
# for some unknown reason,
# it is safe to close it here, it will be reopened automatically.
if self.game_bar:
self.game_bar.destroy() # for gridview only
self.view.set_cursor(Gtk.TreePath('0'), None, False) # needed for both view types
self.view.grab_focus()
@GtkTemplate.Callback
def on_about_clicked(self, *_args):
"""Open the about dialog."""
dialogs.AboutDialog(parent=self)
def on_game_error(self, game, error):
"""Called when a game has sent the 'game-error' signal"""
logger.error("%s crashed", game)
dialogs.ErrorDialog(error, parent=self)
@GtkTemplate.Callback
def on_add_game_button_clicked(self, *_args):
"""Add a new game manually with the AddGameDialog."""
self.application.show_window(AddGamesWindow)
return True
def on_toggle_viewtype(self, *args):
view_type = "list" if self.current_view_type == "grid" else "grid"
logger.debug("View type changed to %s", view_type)
self.set_viewtype_icon(view_type)
settings.write_setting("view_type", view_type)
self.redraw_view()
self._bind_zoom_adjustment()
def on_icontype_state_change(self, action, value):
action.set_state(value)
self._set_icon_type(value.get_string())
def on_view_sorting_state_change(self, action, value):
self.actions["view-sorting"].set_state(value)
value = str(value).strip("'")
settings.write_setting("view_sorting", value)
self.emit("view-updated")
def on_view_sorting_direction_change(self, action, value):
self.actions["view-sorting-ascending"].set_state(value)
settings.write_setting("view_sorting_ascending", bool(value))
self.emit("view-updated")
def on_side_panel_state_change(self, action, value):
"""Callback to handle side panel toggle"""
action.set_state(value)
side_panel_visible = value.get_boolean()
settings.write_setting("side_panel_visible", bool(side_panel_visible))
self.sidebar_revealer.set_reveal_child(side_panel_visible)
def on_sidebar_changed(self, widget):
"""Handler called when the selected element of the sidebar changes"""
for filter_type in ("category", "dynamic_category", "service", "runner", "platform"):
if filter_type in self.filters:
self.filters.pop(filter_type)
row = widget.get_selected_row()
if row:
self.selected_category = "%s:%s" % (row.type, row.id)
self.filters[row.type] = row.id
service_name = self.filters.get("service")
self.set_service(service_name)
self._bind_zoom_adjustment()
self.redraw_view()
def on_game_selection_changed(self, view, selection):
if not selection:
GLib.idle_add(self.update_revealer)
return False
game_id = view.get_model().get_value(selection, COL_ID)
if not game_id:
GLib.idle_add(self.update_revealer)
return False
if self.service:
game = ServiceGameCollection.get_game(self.service.id, game_id)
else:
game = games_db.get_game_by_field(int(game_id), "id")
if not game:
game = {
"id": game_id,
"appid": game_id,
"name": view.get_model().get_value(selection, COL_NAME),
"slug": game_id,
"service": self.service.id if self.service else None,
}
logger.warning("No game found. Replacing with placeholder %s", game)
GLib.idle_add(self.update_revealer, game)
return False
def is_game_displayed(self, game):
"""Return whether a game should be displayed on the view"""
if game.is_hidden and not self.show_hidden_games:
return False
# Stopped games do not get displayed on the running page
if game.state == game.STATE_STOPPED:
selected_row = self.sidebar.get_selected_row()
if selected_row and selected_row.id == "running":
return False
return True
def on_game_updated(self, game):
"""Updates an individual entry in the view when a game is updated"""
if game.appid and self.service:
db_game = ServiceGameCollection.get_game(self.service.id, game.appid)
else:
db_game = games_db.get_game_by_field(game.id, "id")
if not self.is_game_displayed(game):
self.game_store.remove_game(db_game["id"])
return True
updated = self.game_store.update(db_game)
if not updated:
self.update_store()
return True
def on_game_stopped(self, game):
"""Updates the game list when a game stops; this keeps the 'running' page updated."""
selected_row = self.sidebar.get_selected_row()
# Only update the running page- we lose the selected row when we do this,
# but on the running page this is okay.
if selected_row is not None and selected_row.id == "running":
self.game_store.remove_game(game.id)
return True
def on_game_collection_changed(self, _sender):
"""Simple method used to refresh the view"""
self.emit("view-updated")
return True
def on_game_activated(self, view, game_id):
"""Handles view activations (double click, enter press)"""
if self.service:
logger.debug("Looking up %s game %s", self.service.id, game_id)
db_game = games_db.get_game_for_service(self.service.id, game_id)
if self.service.id == "lutris":
if not db_game or not db_game["installed"]:
self.service.install(game_id)
return
game_id = db_game["id"]
else:
if db_game and db_game["installed"]:
game_id = db_game["id"]
else:
service_game = ServiceGameCollection.get_game(self.service.id, game_id)
if not service_game:
logger.error("No game %s found for %s", game_id, self.service.id)
return
game_id = self.service.install(service_game)
if game_id:
game = Game(game_id)
if game.is_installed:
game.emit("game-launch")
else:
game.emit("game-install")
__gtype_name__
special
¶
blank_overlay
¶
current_view_type
property
readonly
¶
Returns which kind of view is currently presented (grid or list)
default_height
¶
default_view_type
¶
default_width
¶
filter_installed
property
readonly
¶
game_revealer
¶
games_scrollwindow
¶
search_entry
¶
service_media
property
readonly
¶
show_hidden_games
property
readonly
¶
show_tray_icon
property
readonly
¶
Setting to hide or show status icon
side_panel_visible
property
readonly
¶
sidebar_revealer
¶
sidebar_scrolled
¶
sort_params
property
readonly
¶
view_sorting
property
readonly
¶
view_sorting_ascending
property
readonly
¶
view_type
property
readonly
¶
Return the type of view saved by the user
viewtype_icon
¶
zoom_adjustment
¶
__init__(self, application, **kwargs)
special
¶
Source code in lutris/gui/lutriswindow.py
def __init__(self, application, **kwargs):
width = int(settings.read_setting("width") or self.default_width)
height = int(settings.read_setting("height") or self.default_height)
super().__init__(
default_width=width,
default_height=height,
window_position=Gtk.WindowPosition.NONE,
name="lutris",
icon_name="lutris",
application=application,
**kwargs
)
update_desktop_icons()
load_icon_theme()
self.application = application
self.window_x = settings.read_setting("window_x")
self.window_y = settings.read_setting("window_y")
if self.window_x and self.window_y:
self.move(int(self.window_x), int(self.window_y))
self.threads_stoppers = []
self.window_size = (width, height)
self.maximized = settings.read_setting("maximized") == "True"
self.service = None
self.game_actions = GameActions(application=application, window=self)
self.search_timer_id = None
self.selected_category = settings.read_setting("selected_category", default="runner:all")
self.filters = self.load_filters()
self.set_service(self.filters.get("service"))
self.icon_type = self.load_icon_type()
self.game_store = GameStore(self.service, self.service_media)
self.view = Gtk.Box()
self.connect("delete-event", self.on_window_delete)
self.connect("configure-event", self.on_window_configure)
self.connect("realize", self.on_load)
if self.maximized:
self.maximize()
self.init_template()
self._init_actions()
self.set_viewtype_icon(self.view_type)
lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
lutris_icon.set_margin_right(3)
self.sidebar = LutrisSidebar(self.application, selected=self.selected_category)
self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
# "realize" is order sensitive- must connect after sidebar itself connects the same signal
self.sidebar.connect("realize", self.on_sidebar_realize)
self.sidebar_scrolled.add(self.sidebar)
# This must wait until the selected-rows-changed signal is connected
self.sidebar.initialize_rows()
self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
self.sidebar_revealer.set_transition_duration(300)
self.game_bar = None
self.revealer_box = Gtk.HBox(visible=True)
self.game_revealer.add(self.revealer_box)
self.connect("view-updated", self.update_store)
GObject.add_emission_hook(BaseService, "service-login", self.on_service_login)
GObject.add_emission_hook(BaseService, "service-logout", self.on_service_logout)
GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
GObject.add_emission_hook(Game, "game-stopped", self.on_game_stopped)
GObject.add_emission_hook(Game, "game-removed", self.on_game_collection_changed)
combine_games(service_game, lutris_game)
staticmethod
¶
Inject lutris game information into a service game
Source code in lutris/gui/lutriswindow.py
@staticmethod
def combine_games(service_game, lutris_game):
"""Inject lutris game information into a service game"""
if lutris_game and service_game["appid"] == lutris_game["service_id"]:
for field in ("platform", "runner", "year", "installed_at", "lastplayed", "playtime", "installed"):
service_game[field] = lutris_game[field]
return service_game
do_key_press_event(self, event)
¶
key_press_event(self, event:Gdk.EventKey) -> bool
Source code in lutris/gui/lutriswindow.py
def do_key_press_event(self, event): # pylint: disable=arguments-differ
# XXX: This block of code below is to enable searching on type.
# Enabling this feature steals focus from other entries so it needs
# some kind of focus detection before enabling library search.
# Probably not ideal for non-english, but we want to limit
# which keys actually start searching
if event.keyval == Gdk.KEY_Escape:
self.search_entry.set_text("")
self.view.grab_focus()
return Gtk.ApplicationWindow.do_key_press_event(self, event)
if ( # pylint: disable=too-many-boolean-expressions
not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z or event.state & Gdk.ModifierType.CONTROL_MASK
or event.state & Gdk.ModifierType.SHIFT_MASK or event.state & Gdk.ModifierType.META_MASK
or event.state & Gdk.ModifierType.MOD1_MASK or self.search_entry.has_focus()
):
return Gtk.ApplicationWindow.do_key_press_event(self, event)
self.search_entry.grab_focus()
return self.search_entry.do_key_press_event(self.search_entry, event)
game_matches(self, game)
¶
Source code in lutris/gui/lutriswindow.py
def game_matches(self, game):
if self.filters.get("installed"):
if game["appid"] not in games_db.get_service_games(self.service.id):
return False
if not self.filters.get("text"):
return True
return self.filters["text"] in game["name"].lower()
get_games_from_filters(self)
¶
Source code in lutris/gui/lutriswindow.py
def get_games_from_filters(self):
service_name = self.filters.get("service")
if service_name in services.SERVICES:
if self.service.online and not self.service.is_authenticated():
self.show_label(_("Connect your %s account to access your games") % self.service.name)
return []
return self.get_service_games(service_name)
dynamic_categories = {
"recent": self.get_recent_games,
"running": self.get_running_games,
}
if self.filters.get("dynamic_category") in dynamic_categories:
return dynamic_categories[self.filters["dynamic_category"]]()
if self.filters.get("category") and self.filters["category"] != "all":
game_ids = categories_db.get_game_ids_for_category(self.filters["category"])
else:
game_ids = None
searches, filters, excludes = self.get_sql_filters()
games = games_db.get_games(
searches=searches,
filters=filters,
excludes=excludes,
sorts=self.sort_params
)
if game_ids is not None:
return [game for game in games if game["id"] in game_ids]
return games
get_recent_games(self)
¶
Return a list of currently running games
Source code in lutris/gui/lutriswindow.py
def get_recent_games(self):
"""Return a list of currently running games"""
searches, _filters, excludes = self.get_sql_filters()
games = games_db.get_games(searches=searches, filters={'installed': '1'}, excludes=excludes)
return sorted(
games,
key=lambda game: max(game["installed_at"] or 0, game["lastplayed"] or 0),
reverse=True
)
get_running_games(self)
¶
Return a list of currently running games
Source code in lutris/gui/lutriswindow.py
def get_running_games(self):
"""Return a list of currently running games"""
return games_db.get_games_by_ids([game.id for game in self.application.running_games])
get_service_games(self, service_name)
¶
Switch the current service to service_name and return games if available
Source code in lutris/gui/lutriswindow.py
def get_service_games(self, service_name):
"""Switch the current service to service_name and return games if available"""
service_games = ServiceGameCollection.get_for_service(service_name)
if service_name == "lutris":
lutris_games = {g["slug"]: g for g in games_db.get_games()}
else:
lutris_games = {g["service_id"]: g for g in games_db.get_games(filters={"service": self.service.id})}
def get_sort_value(game):
sort_defaults = {
"name": "",
"year": 0,
"lastplayed": 0.0,
"installed_at": 0.0,
"playtime": 0.0,
}
view_sorting = self.view_sorting
lutris_game = lutris_games.get(game["appid"])
if not lutris_game:
return sort_defaults.get(view_sorting, "")
value = lutris_game.get(view_sorting)
if value:
return value
# Users may have obsolete view_sorting settings, so
# we must tolerate them. We treat them all as blank.
return sort_defaults.get(view_sorting, "")
return [
self.combine_games(game, lutris_games.get(game["appid"])) for game in sorted(
service_games,
key=get_sort_value,
reverse=not self.view_sorting_ascending
) if self.game_matches(game)
]
get_service_media(self, icon_type)
¶
Return the ServiceMedia class used for this view
Source code in lutris/gui/lutriswindow.py
def get_service_media(self, icon_type):
"""Return the ServiceMedia class used for this view"""
service = self.service if self.service else LutrisService
medias = service.medias
if icon_type in medias:
return medias[icon_type]()
return medias[service.default_format]()
get_sql_filters(self)
¶
Return the current filters for the view
Source code in lutris/gui/lutriswindow.py
def get_sql_filters(self):
"""Return the current filters for the view"""
sql_filters = {}
sql_excludes = {}
if self.filters.get("runner"):
sql_filters["runner"] = self.filters["runner"]
if self.filters.get("platform"):
sql_filters["platform"] = self.filters["platform"]
if self.filters.get("installed"):
sql_filters["installed"] = "1"
if self.filters.get("text"):
searches = {"name": self.filters["text"]}
else:
searches = None
if not self.filters.get("hidden"):
sql_excludes["hidden"] = 1
return searches, sql_filters, sql_excludes
hidden_state_change(self, action, value)
¶
Hides or shows the hidden games
Source code in lutris/gui/lutriswindow.py
def hidden_state_change(self, action, value):
"""Hides or shows the hidden games"""
action.set_state(value)
settings.write_setting("show_hidden_games", str(value).lower(), section="lutris")
self.filters["hidden"] = value
self.emit("view-updated")
hide_overlay(self)
¶
Source code in lutris/gui/lutriswindow.py
def hide_overlay(self):
self.blank_overlay.props.visible = False
for child in self.blank_overlay.get_children():
child.destroy()
init_template(s)
¶
Source code in lutris/gui/lutriswindow.py
cls.init_template = lambda s: _init_template(s, cls, base_init_template)
is_game_displayed(self, game)
¶
Return whether a game should be displayed on the view
Source code in lutris/gui/lutriswindow.py
def is_game_displayed(self, game):
"""Return whether a game should be displayed on the view"""
if game.is_hidden and not self.show_hidden_games:
return False
# Stopped games do not get displayed on the running page
if game.state == game.STATE_STOPPED:
selected_row = self.sidebar.get_selected_row()
if selected_row and selected_row.id == "running":
return False
return True
load_filters(self)
¶
Load the initial filters when creating the view
Source code in lutris/gui/lutriswindow.py
def load_filters(self):
"""Load the initial filters when creating the view"""
category, value = self.selected_category.split(":")
filters = {
category: value
} # Type of filter corresponding to the selected sidebar element
filters["hidden"] = settings.read_setting("show_hidden_games").lower() == "true"
filters["installed"] = settings.read_setting("filter_installed").lower() == "true"
return filters
load_icon_type(self)
¶
Return the icon style depending on the type of view.
Source code in lutris/gui/lutriswindow.py
def load_icon_type(self):
"""Return the icon style depending on the type of view."""
setting_key = "icon_type_%sview" % self.current_view_type
if self.service and self.service.id != "lutris":
setting_key += "_%s" % self.service.id
self.icon_type = settings.read_setting(setting_key)
return self.icon_type
on_about_clicked(self, *_args)
¶
Open the about dialog.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_about_clicked(self, *_args):
"""Open the about dialog."""
dialogs.AboutDialog(parent=self)
on_add_game_button_clicked(self, *_args)
¶
Add a new game manually with the AddGameDialog.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_add_game_button_clicked(self, *_args):
"""Add a new game manually with the AddGameDialog."""
self.application.show_window(AddGamesWindow)
return True
on_destroy(self, *_args)
¶
Signal for window close.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_destroy(self, *_args):
"""Signal for window close."""
# Stop cancellable running threads
for stopper in self.threads_stoppers:
stopper()
# Save settings
width, height = self.window_size
settings.write_setting("width", width)
settings.write_setting("height", height)
if self.window_x and self.window_y:
settings.write_setting("window_x", self.window_x)
settings.write_setting("window_y", self.window_y)
settings.write_setting("maximized", self.maximized)
on_game_activated(self, view, game_id)
¶
Handles view activations (double click, enter press)
Source code in lutris/gui/lutriswindow.py
def on_game_activated(self, view, game_id):
"""Handles view activations (double click, enter press)"""
if self.service:
logger.debug("Looking up %s game %s", self.service.id, game_id)
db_game = games_db.get_game_for_service(self.service.id, game_id)
if self.service.id == "lutris":
if not db_game or not db_game["installed"]:
self.service.install(game_id)
return
game_id = db_game["id"]
else:
if db_game and db_game["installed"]:
game_id = db_game["id"]
else:
service_game = ServiceGameCollection.get_game(self.service.id, game_id)
if not service_game:
logger.error("No game %s found for %s", game_id, self.service.id)
return
game_id = self.service.install(service_game)
if game_id:
game = Game(game_id)
if game.is_installed:
game.emit("game-launch")
else:
game.emit("game-install")
on_game_collection_changed(self, _sender)
¶
Simple method used to refresh the view
Source code in lutris/gui/lutriswindow.py
def on_game_collection_changed(self, _sender):
"""Simple method used to refresh the view"""
self.emit("view-updated")
return True
on_game_error(self, game, error)
¶
Called when a game has sent the 'game-error' signal
Source code in lutris/gui/lutriswindow.py
def on_game_error(self, game, error):
"""Called when a game has sent the 'game-error' signal"""
logger.error("%s crashed", game)
dialogs.ErrorDialog(error, parent=self)
on_game_selection_changed(self, view, selection)
¶
Source code in lutris/gui/lutriswindow.py
def on_game_selection_changed(self, view, selection):
if not selection:
GLib.idle_add(self.update_revealer)
return False
game_id = view.get_model().get_value(selection, COL_ID)
if not game_id:
GLib.idle_add(self.update_revealer)
return False
if self.service:
game = ServiceGameCollection.get_game(self.service.id, game_id)
else:
game = games_db.get_game_by_field(int(game_id), "id")
if not game:
game = {
"id": game_id,
"appid": game_id,
"name": view.get_model().get_value(selection, COL_NAME),
"slug": game_id,
"service": self.service.id if self.service else None,
}
logger.warning("No game found. Replacing with placeholder %s", game)
GLib.idle_add(self.update_revealer, game)
return False
on_game_stopped(self, game)
¶
Updates the game list when a game stops; this keeps the 'running' page updated.
Source code in lutris/gui/lutriswindow.py
def on_game_stopped(self, game):
"""Updates the game list when a game stops; this keeps the 'running' page updated."""
selected_row = self.sidebar.get_selected_row()
# Only update the running page- we lose the selected row when we do this,
# but on the running page this is okay.
if selected_row is not None and selected_row.id == "running":
self.game_store.remove_game(game.id)
return True
on_game_updated(self, game)
¶
Updates an individual entry in the view when a game is updated
Source code in lutris/gui/lutriswindow.py
def on_game_updated(self, game):
"""Updates an individual entry in the view when a game is updated"""
if game.appid and self.service:
db_game = ServiceGameCollection.get_game(self.service.id, game.appid)
else:
db_game = games_db.get_game_by_field(game.id, "id")
if not self.is_game_displayed(game):
self.game_store.remove_game(db_game["id"])
return True
updated = self.game_store.update(db_game)
if not updated:
self.update_store()
return True
on_icontype_state_change(self, action, value)
¶
Source code in lutris/gui/lutriswindow.py
def on_icontype_state_change(self, action, value):
action.set_state(value)
self._set_icon_type(value.get_string())
on_load(self, widget, data=None)
¶
Finish initializing the view
Source code in lutris/gui/lutriswindow.py
def on_load(self, widget, data=None):
"""Finish initializing the view"""
self._bind_zoom_adjustment()
self.view.grab_focus()
self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
on_preferences_activate(self, *_args)
¶
Callback when preferences is activated.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_preferences_activate(self, *_args):
"""Callback when preferences is activated."""
self.application.show_window(PreferencesDialog)
on_resize(self, widget, *_args)
¶
Size-allocate signal. Updates stored window size and maximized state.
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_resize(self, widget, *_args):
"""Size-allocate signal.
Updates stored window size and maximized state.
"""
if not widget.get_window():
return
self.maximized = widget.is_maximized()
size = widget.get_size()
if not self.maximized:
self.window_size = size
self.search_entry.set_size_request(min(max(50, size[0] - 470), 800), -1)
on_search_entry_changed(self, entry)
¶
Callback for the search input keypresses
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_search_entry_changed(self, entry):
"""Callback for the search input keypresses"""
if self.search_timer_id:
GLib.source_remove(self.search_timer_id)
self.filters["text"] = entry.get_text().lower().strip()
self.search_timer_id = GLib.timeout_add(150, self.update_store)
on_search_entry_key_press(self, widget, event)
¶
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_search_entry_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Down:
if self.current_view_type == 'grid':
self.view.select_path(Gtk.TreePath('0')) # needed for gridview only
# if game_bar is alive at this point it can mess grid item selection up
# for some unknown reason,
# it is safe to close it here, it will be reopened automatically.
if self.game_bar:
self.game_bar.destroy() # for gridview only
self.view.set_cursor(Gtk.TreePath('0'), None, False) # needed for both view types
self.view.grab_focus()
on_service_games_updated(self, service)
¶
Request a view update when service games are loaded
Source code in lutris/gui/lutriswindow.py
def on_service_games_updated(self, service):
"""Request a view update when service games are loaded"""
if self.service and service.id == self.service.id:
self.emit("view-updated")
return True
on_service_login(self, service)
¶
Source code in lutris/gui/lutriswindow.py
def on_service_login(self, service):
AsyncCall(service.reload, None)
return True
on_service_logout(self, service)
¶
Source code in lutris/gui/lutriswindow.py
def on_service_logout(self, service):
if self.service and service.id == self.service.id:
self.emit("view-updated")
return True
on_show_installed_state_change(self, action, value)
¶
Callback to handle uninstalled game filter switch
Source code in lutris/gui/lutriswindow.py
def on_show_installed_state_change(self, action, value):
"""Callback to handle uninstalled game filter switch"""
action.set_state(value)
self.set_show_installed_state(value.get_boolean())
self.emit("view-updated")
on_side_panel_state_change(self, action, value)
¶
Callback to handle side panel toggle
Source code in lutris/gui/lutriswindow.py
def on_side_panel_state_change(self, action, value):
"""Callback to handle side panel toggle"""
action.set_state(value)
side_panel_visible = value.get_boolean()
settings.write_setting("side_panel_visible", bool(side_panel_visible))
self.sidebar_revealer.set_reveal_child(side_panel_visible)
on_sidebar_changed(self, widget)
¶
Handler called when the selected element of the sidebar changes
Source code in lutris/gui/lutriswindow.py
def on_sidebar_changed(self, widget):
"""Handler called when the selected element of the sidebar changes"""
for filter_type in ("category", "dynamic_category", "service", "runner", "platform"):
if filter_type in self.filters:
self.filters.pop(filter_type)
row = widget.get_selected_row()
if row:
self.selected_category = "%s:%s" % (row.type, row.id)
self.filters[row.type] = row.id
service_name = self.filters.get("service")
self.set_service(service_name)
self._bind_zoom_adjustment()
self.redraw_view()
on_sidebar_realize(self, widget, data=None)
¶
Grab the initial focus after the sidebar is initialized - so the view is ready.
Source code in lutris/gui/lutriswindow.py
def on_sidebar_realize(self, widget, data=None):
"""Grab the initial focus after the sidebar is initialized - so the view is ready."""
self.view.grab_focus()
on_toggle_viewtype(self, *args)
¶
Source code in lutris/gui/lutriswindow.py
def on_toggle_viewtype(self, *args):
view_type = "list" if self.current_view_type == "grid" else "grid"
logger.debug("View type changed to %s", view_type)
self.set_viewtype_icon(view_type)
settings.write_setting("view_type", view_type)
self.redraw_view()
self._bind_zoom_adjustment()
on_view_sorting_direction_change(self, action, value)
¶
Source code in lutris/gui/lutriswindow.py
def on_view_sorting_direction_change(self, action, value):
self.actions["view-sorting-ascending"].set_state(value)
settings.write_setting("view_sorting_ascending", bool(value))
self.emit("view-updated")
on_view_sorting_state_change(self, action, value)
¶
Source code in lutris/gui/lutriswindow.py
def on_view_sorting_state_change(self, action, value):
self.actions["view-sorting"].set_state(value)
value = str(value).strip("'")
settings.write_setting("view_sorting", value)
self.emit("view-updated")
on_window_configure(self, *_args)
¶
Callback triggered when the window is moved, resized...
Source code in lutris/gui/lutriswindow.py
def on_window_configure(self, *_args):
"""Callback triggered when the window is moved, resized..."""
self.window_x, self.window_y = self.get_position()
on_window_delete(self, *_args)
¶
Source code in lutris/gui/lutriswindow.py
def on_window_delete(self, *_args):
if self.application.running_games.get_n_items():
self.hide()
return True
on_zoom_changed(self, adjustment)
¶
Handler for zoom modification
Source code in lutris/gui/lutriswindow.py
def on_zoom_changed(self, adjustment):
"""Handler for zoom modification"""
media_index = round(adjustment.props.value)
adjustment.props.value = media_index
service = self.service if self.service else LutrisService
media_services = list(service.medias.keys())
if len(media_services) <= media_index:
media_index = media_services.index(service.default_format)
icon_type = media_services[media_index]
if icon_type != self.icon_type:
self.save_icon_type(icon_type)
self.show_spinner()
redraw_view(self)
¶
Completely reconstruct the main view
Source code in lutris/gui/lutriswindow.py
def redraw_view(self):
"""Completely reconstruct the main view"""
if not self.game_store:
logger.error("No game store yet")
return
if self.view:
self.view.destroy()
self.game_store = GameStore(self.service, self.service_media)
if self.view_type == "grid":
self.view = GameGridView(
self.game_store,
self.game_store.service_media,
hide_text=settings.read_setting("hide_text_under_icons") == "True"
)
else:
self.view = GameListView(self.game_store, self.game_store.service_media)
self.view.connect("game-selected", self.on_game_selection_changed)
self.view.connect("game-activated", self.on_game_activated)
self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
for child in self.games_scrollwindow.get_children():
child.destroy()
self.games_scrollwindow.add(self.view)
self.view.show_all()
self.update_store()
save_icon_type(self, icon_type)
¶
Save icon type to settings
Source code in lutris/gui/lutriswindow.py
def save_icon_type(self, icon_type):
"""Save icon type to settings"""
self.icon_type = icon_type
setting_key = "icon_type_%sview" % self.current_view_type
if self.service and self.service.id != "lutris":
setting_key += "_%s" % self.service.id
settings.write_setting(setting_key, self.icon_type)
self.redraw_view()
set_service(self, service_name)
¶
Source code in lutris/gui/lutriswindow.py
def set_service(self, service_name):
if self.service and self.service.id == service_name:
return self.service
if not service_name:
self.service = None
return
try:
self.service = services.SERVICES[service_name]()
except KeyError:
logger.error("Non existent service '%s'", service_name)
self.service = None
return self.service
set_show_installed_state(self, filter_installed)
¶
Shows or hide uninstalled games
Source code in lutris/gui/lutriswindow.py
def set_show_installed_state(self, filter_installed):
"""Shows or hide uninstalled games"""
settings.write_setting("filter_installed", bool(filter_installed))
self.filters["installed"] = filter_installed
set_viewtype_icon(self, view_type)
¶
Source code in lutris/gui/lutriswindow.py
def set_viewtype_icon(self, view_type):
self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type, Gtk.IconSize.BUTTON)
show_empty_label(self)
¶
Display a label when the view is empty
Source code in lutris/gui/lutriswindow.py
def show_empty_label(self):
"""Display a label when the view is empty"""
if self.filters.get("text"):
self.show_label(_("No games matching '%s' found ") % self.filters["text"])
else:
if self.filters.get("category") == "favorite":
self.show_label(_("Add games to your favorites to see them here."))
elif self.filters.get("installed"):
self.show_label(_("No installed games found. Press Ctrl+H so show all games."))
else:
self.show_splash()
# self.show_label(_("No games found"))
show_label(self, message)
¶
Display a label in the middle of the UI
Source code in lutris/gui/lutriswindow.py
def show_label(self, message):
"""Display a label in the middle of the UI"""
self.show_overlay(Gtk.Label(message, visible=True))
show_overlay(self, widget, halign=<enum GTK_ALIGN_FILL of type Gtk.Align>, valign=<enum GTK_ALIGN_FILL of type Gtk.Align>)
¶
Display a widget in the blank overlay
Source code in lutris/gui/lutriswindow.py
def show_overlay(self, widget, halign=Gtk.Align.FILL, valign=Gtk.Align.FILL):
"""Display a widget in the blank overlay"""
for child in self.blank_overlay.get_children():
child.destroy()
self.blank_overlay.set_halign(halign)
self.blank_overlay.set_valign(valign)
self.blank_overlay.add(widget)
self.blank_overlay.props.visible = True
show_spinner(self)
¶
Source code in lutris/gui/lutriswindow.py
def show_spinner(self):
spinner = Gtk.Spinner(visible=True)
spinner.start()
for child in self.blank_overlay.get_children():
child.destroy()
self.blank_overlay.add(spinner)
self.blank_overlay.props.visible = True
show_splash(self)
¶
Source code in lutris/gui/lutriswindow.py
def show_splash(self):
image = Gtk.Image(visible=True)
image.set_from_file(os.path.join(datapath.get(), "media/splash.svg"))
self.show_overlay(image, Gtk.Align.START, Gtk.Align.START)
update_revealer(self, game=None)
¶
Source code in lutris/gui/lutriswindow.py
def update_revealer(self, game=None):
if game:
if self.game_bar:
self.game_bar.destroy()
self.game_bar = GameBar(game, self.game_actions, self.application)
self.revealer_box.pack_start(self.game_bar, True, True, 0)
elif self.game_bar:
# The game bar can't be destroyed here because the game gets unselected on Wayland
# whenever the game bar is interacted with. Instead, we keep the current game bar open
# when the game gets unselected, which is somewhat closer to what the intended behavior
# should be anyway. Might require closing the game bar manually in some cases.
pass
# self.game_bar.destroy()
if self.revealer_box.get_children():
self.game_revealer.set_reveal_child(True)
else:
self.game_revealer.set_reveal_child(False)
update_store(self, *_args, **_kwargs)
¶
Source code in lutris/gui/lutriswindow.py
def update_store(self, *_args, **_kwargs):
self.game_store.store.clear()
for child in self.blank_overlay.get_children():
child.destroy()
games = self.get_games_from_filters()
logger.debug("Showing %d games", len(games))
self.view.service = self.service.id if self.service else None
GLib.idle_add(self.update_revealer)
for game in games:
self.game_store.add_game(game)
if not games:
self.show_empty_label()
self.search_timer_id = None
return False
views
special
¶
Common values used for views
COLUMN_NAMES
¶
COL_ICON
¶
COL_ID
¶
COL_INSTALLED
¶
COL_INSTALLED_AT
¶
COL_INSTALLED_AT_TEXT
¶
COL_LASTPLAYED
¶
COL_LASTPLAYED_TEXT
¶
COL_NAME
¶
COL_PLATFORM
¶
COL_PLAYTIME
¶
COL_PLAYTIME_TEXT
¶
COL_RUNNER
¶
COL_RUNNER_HUMAN_NAME
¶
COL_SLUG
¶
COL_YEAR
¶
base
¶
GameView
¶
Source code in lutris/gui/views/base.py
class GameView:
# pylint: disable=no-member
__gsignals__ = {
"game-selected": (GObject.SIGNAL_RUN_FIRST, None, (Gtk.TreeIter, )),
"game-activated": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
"remove-game": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self):
self.service = None # Stores the service.id in a string
self.current_path = None
self.contextual_menu = None
def connect_signals(self):
"""Signal handlers common to all views"""
self.connect("button-press-event", self.popup_contextual_menu)
self.connect("key-press-event", self.handle_key_press)
def popup_contextual_menu(self, view, event):
"""Contextual menu."""
if event.button != 3:
return
view.current_path = view.get_path_at_pos(event.x, event.y)
if view.current_path:
view.select()
_iter = self.get_model().get_iter(view.current_path[0])
if not _iter:
return
selected_id = self.get_selected_id(_iter)
game_row = self.game_store.get_row_by_id(selected_id)
game_id = None
if self.service:
game = get_game_for_service(self.service, game_row[COL_ID])
if game:
game_id = game["id"]
else:
game_id = game_row[COL_ID]
if not game_id:
return
game = Game(game_id)
game_actions = GameActions()
game_actions.set_game(game=game)
self.contextual_menu.popup(event, game_actions)
def get_selected_id(self, selected_item):
return self.get_model().get_value(selected_item, COL_ID)
def select(self):
"""Selects the object pointed by current_path"""
raise NotImplementedError
def handle_key_press(self, widget, event): # pylint: disable=unused-argument
key = event.keyval
if key == Gdk.KEY_Delete:
self.emit("remove-game")
__gsignals__
special
¶
__init__(self)
special
¶
Source code in lutris/gui/views/base.py
def __init__(self):
self.service = None # Stores the service.id in a string
self.current_path = None
self.contextual_menu = None
connect_signals(self)
¶
Signal handlers common to all views
Source code in lutris/gui/views/base.py
def connect_signals(self):
"""Signal handlers common to all views"""
self.connect("button-press-event", self.popup_contextual_menu)
self.connect("key-press-event", self.handle_key_press)
get_selected_id(self, selected_item)
¶
Source code in lutris/gui/views/base.py
def get_selected_id(self, selected_item):
return self.get_model().get_value(selected_item, COL_ID)
handle_key_press(self, widget, event)
¶
Source code in lutris/gui/views/base.py
def handle_key_press(self, widget, event): # pylint: disable=unused-argument
key = event.keyval
if key == Gdk.KEY_Delete:
self.emit("remove-game")
popup_contextual_menu(self, view, event)
¶
Contextual menu.
Source code in lutris/gui/views/base.py
def popup_contextual_menu(self, view, event):
"""Contextual menu."""
if event.button != 3:
return
view.current_path = view.get_path_at_pos(event.x, event.y)
if view.current_path:
view.select()
_iter = self.get_model().get_iter(view.current_path[0])
if not _iter:
return
selected_id = self.get_selected_id(_iter)
game_row = self.game_store.get_row_by_id(selected_id)
game_id = None
if self.service:
game = get_game_for_service(self.service, game_row[COL_ID])
if game:
game_id = game["id"]
else:
game_id = game_row[COL_ID]
if not game_id:
return
game = Game(game_id)
game_actions = GameActions()
game_actions.set_game(game=game)
self.contextual_menu.popup(event, game_actions)
select(self)
¶
Selects the object pointed by current_path
Source code in lutris/gui/views/base.py
def select(self):
"""Selects the object pointed by current_path"""
raise NotImplementedError
grid
¶
Grid view for the main window
GameGridView (IconView, GameView)
¶
Source code in lutris/gui/views/grid.py
class GameGridView(Gtk.IconView, GameView):
__gsignals__ = GameView.__gsignals__
min_width = 70 # Minimum width for a cell
def __init__(self, store, service_media, hide_text=False):
self.game_store = store
self.service_media = service_media
self.model = self.game_store.store
super().__init__(model=self.game_store.store)
GameView.__init__(self)
self.service = None
self.set_column_spacing(6)
self.set_pixbuf_column(COL_ICON)
self.set_item_padding(1)
self.cell_width = max(service_media.size[0], self.min_width)
if hide_text:
self.cell_renderer = None
else:
self.cell_renderer = GridViewCellRendererText(self.cell_width)
self.pack_end(self.cell_renderer, False)
self.add_attribute(self.cell_renderer, "markup", COL_NAME)
self.connect_signals()
self.connect("item-activated", self.on_item_activated)
self.connect("selection-changed", self.on_selection_changed)
def select(self):
self.select_path(self.current_path)
def get_selected_item(self):
"""Return the currently selected game's id."""
selection = self.get_selected_items()
if not selection:
return
self.current_path = selection[0]
return self.get_model().get_iter(self.current_path)
def on_item_activated(self, _view, _path):
"""Handles double clicks"""
selected_item = self.get_selected_item()
if selected_item:
selected_id = self.get_selected_id(selected_item)
else:
selected_id = None
logger.debug("Item activated: %s", selected_id)
self.emit("game-activated", selected_id)
def on_selection_changed(self, _view):
"""Handles selection changes"""
selected_items = self.get_selected_item()
if selected_items:
self.emit("game-selected", selected_items)
min_width
¶
__init__(self, store, service_media, hide_text=False)
special
¶
Source code in lutris/gui/views/grid.py
def __init__(self, store, service_media, hide_text=False):
self.game_store = store
self.service_media = service_media
self.model = self.game_store.store
super().__init__(model=self.game_store.store)
GameView.__init__(self)
self.service = None
self.set_column_spacing(6)
self.set_pixbuf_column(COL_ICON)
self.set_item_padding(1)
self.cell_width = max(service_media.size[0], self.min_width)
if hide_text:
self.cell_renderer = None
else:
self.cell_renderer = GridViewCellRendererText(self.cell_width)
self.pack_end(self.cell_renderer, False)
self.add_attribute(self.cell_renderer, "markup", COL_NAME)
self.connect_signals()
self.connect("item-activated", self.on_item_activated)
self.connect("selection-changed", self.on_selection_changed)
get_selected_item(self)
¶
Return the currently selected game's id.
Source code in lutris/gui/views/grid.py
def get_selected_item(self):
"""Return the currently selected game's id."""
selection = self.get_selected_items()
if not selection:
return
self.current_path = selection[0]
return self.get_model().get_iter(self.current_path)
on_item_activated(self, _view, _path)
¶
Handles double clicks
Source code in lutris/gui/views/grid.py
def on_item_activated(self, _view, _path):
"""Handles double clicks"""
selected_item = self.get_selected_item()
if selected_item:
selected_id = self.get_selected_id(selected_item)
else:
selected_id = None
logger.debug("Item activated: %s", selected_id)
self.emit("game-activated", selected_id)
on_selection_changed(self, _view)
¶
Handles selection changes
Source code in lutris/gui/views/grid.py
def on_selection_changed(self, _view):
"""Handles selection changes"""
selected_items = self.get_selected_item()
if selected_items:
self.emit("game-selected", selected_items)
select(self)
¶
Selects the object pointed by current_path
Source code in lutris/gui/views/grid.py
def select(self):
self.select_path(self.current_path)
list
¶
TreeView based game list
GameListColumnToggleMenu (Menu)
¶
Source code in lutris/gui/views/list.py
class GameListColumnToggleMenu(Gtk.Menu):
def __init__(self, columns):
super().__init__()
self.columns = columns
self.column_map = {}
self.create_menuitems()
self.show_all()
def create_menuitems(self):
for column in self.columns:
title = column.get_title()
if title == "":
continue
checkbox = Gtk.CheckMenuItem(title)
checkbox.set_active(column.get_visible())
if title == _("Name"):
checkbox.set_sensitive(False)
else:
checkbox.connect("toggled", self.on_toggle_column)
self.column_map[checkbox] = column
self.append(checkbox)
def on_toggle_column(self, check_menu_item):
column = self.column_map[check_menu_item]
is_visible = check_menu_item.get_active()
column.set_visible(is_visible)
settings.write_setting(
column.get_title().replace(" ", "") + "_visible",
str(is_visible),
"list view",
)
__init__(self, columns)
special
¶
Source code in lutris/gui/views/list.py
def __init__(self, columns):
super().__init__()
self.columns = columns
self.column_map = {}
self.create_menuitems()
self.show_all()
create_menuitems(self)
¶
Source code in lutris/gui/views/list.py
def create_menuitems(self):
for column in self.columns:
title = column.get_title()
if title == "":
continue
checkbox = Gtk.CheckMenuItem(title)
checkbox.set_active(column.get_visible())
if title == _("Name"):
checkbox.set_sensitive(False)
else:
checkbox.connect("toggled", self.on_toggle_column)
self.column_map[checkbox] = column
self.append(checkbox)
on_toggle_column(self, check_menu_item)
¶
Source code in lutris/gui/views/list.py
def on_toggle_column(self, check_menu_item):
column = self.column_map[check_menu_item]
is_visible = check_menu_item.get_active()
column.set_visible(is_visible)
settings.write_setting(
column.get_title().replace(" ", "") + "_visible",
str(is_visible),
"list view",
)
GameListView (TreeView, GameView)
¶
Show the main list of games.
Source code in lutris/gui/views/list.py
class GameListView(Gtk.TreeView, GameView):
"""Show the main list of games."""
__gsignals__ = GameView.__gsignals__
def __init__(self, store, service_media):
self.game_store = store
self.service_media = service_media
self.model = self.game_store.store
super().__init__(model=self.model)
GameView.__init__(self)
self.set_rules_hint(True)
# Icon column
if settings.SHOW_MEDIA:
image_cell = Gtk.CellRendererPixbuf()
column = Gtk.TreeViewColumn("", image_cell, pixbuf=COL_ICON)
column.set_reorderable(True)
column.set_sort_indicator(False)
self.append_column(column)
# Text columns
default_text_cell = self.set_text_cell()
name_cell = self.set_text_cell()
name_cell.set_padding(5, 0)
self.set_column(name_cell, _("Name"), COL_NAME, 200, always_visible=True)
self.set_column(default_text_cell, _("Year"), COL_YEAR, 60)
self.set_column(default_text_cell, _("Runner"), COL_RUNNER_HUMAN_NAME, 120)
self.set_column(default_text_cell, _("Platform"), COL_PLATFORM, 120)
self.set_column(default_text_cell, _("Last Played"), COL_LASTPLAYED_TEXT, 120)
self.set_sort_with_column(COL_LASTPLAYED_TEXT, COL_LASTPLAYED)
self.set_column(default_text_cell, _("Installed At"), COL_INSTALLED_AT_TEXT, 120)
self.set_sort_with_column(COL_INSTALLED_AT_TEXT, COL_INSTALLED_AT)
self.set_column(default_text_cell, _("Play Time"), COL_PLAYTIME_TEXT, 100)
self.set_sort_with_column(COL_PLAYTIME_TEXT, COL_PLAYTIME)
self.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
self.connect_signals()
self.connect("row-activated", self.on_row_activated)
self.get_selection().connect("changed", self.on_cursor_changed)
@staticmethod
def set_text_cell():
text_cell = Gtk.CellRendererText()
text_cell.set_padding(10, 0)
text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
return text_cell
def set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None):
column = Gtk.TreeViewColumn(header, cell, markup=column_id)
column.set_sort_indicator(True)
column.set_sort_column_id(column_id if sort_id is None else sort_id)
self.set_column_sort(column_id if sort_id is None else sort_id)
column.set_resizable(True)
column.set_reorderable(True)
width = settings.read_setting("%s_column_width" % COLUMN_NAMES[column_id], "list view")
is_visible = settings.read_setting("%s_visible" % COLUMN_NAMES[column_id], "list view")
column.set_fixed_width(int(width) if width else default_width)
column.set_visible(is_visible == "True" or always_visible if is_visible else True)
self.append_column(column)
column.connect("notify::width", self.on_column_width_changed)
column.get_button().connect('button-press-event', self.on_column_header_button_pressed)
return column
def set_column_sort(self, col):
"""Sort a column and fallback to sorting by name and runner."""
model = self.get_model()
if model:
model.set_sort_func(col, sort_func, col)
def set_sort_with_column(self, col, sort_col):
"""Sort a column by using another column's data"""
self.model.set_sort_func(col, sort_func, sort_col)
def get_selected_item(self):
"""Return the currently selected game's id."""
selection = self.get_selection()
if not selection:
return None
_model, select_iter = selection.get_selected()
if select_iter:
return select_iter
def select(self):
self.set_cursor(self.current_path[0])
def set_selected_game(self, game_id):
row = self.game_store.get_row_by_id(game_id, filtered=True)
if row:
self.set_cursor(row.path)
def on_column_header_button_pressed(self, button, event):
"""Handles column header button press events"""
if event.button == 3:
menu = GameListColumnToggleMenu(self.get_columns())
menu.popup_at_pointer(None)
return True
def on_row_activated(self, widget, line=None, column=None):
"""Handles double clicks"""
selected_item = self.get_selected_item()
if selected_item:
selected_id = self.get_selected_id(selected_item)
else:
selected_id = None
self.emit("game-activated", selected_id)
def on_cursor_changed(self, widget, _line=None, _column=None):
selected_item = self.get_selected_item()
self.emit("game-selected", selected_item)
@staticmethod
def on_column_width_changed(col, *args):
col_name = col.get_title()
if col_name:
settings.write_setting(
col_name.replace(" ", "") + "_column_width",
col.get_fixed_width(),
"list view",
)
__init__(self, store, service_media)
special
¶
Source code in lutris/gui/views/list.py
def __init__(self, store, service_media):
self.game_store = store
self.service_media = service_media
self.model = self.game_store.store
super().__init__(model=self.model)
GameView.__init__(self)
self.set_rules_hint(True)
# Icon column
if settings.SHOW_MEDIA:
image_cell = Gtk.CellRendererPixbuf()
column = Gtk.TreeViewColumn("", image_cell, pixbuf=COL_ICON)
column.set_reorderable(True)
column.set_sort_indicator(False)
self.append_column(column)
# Text columns
default_text_cell = self.set_text_cell()
name_cell = self.set_text_cell()
name_cell.set_padding(5, 0)
self.set_column(name_cell, _("Name"), COL_NAME, 200, always_visible=True)
self.set_column(default_text_cell, _("Year"), COL_YEAR, 60)
self.set_column(default_text_cell, _("Runner"), COL_RUNNER_HUMAN_NAME, 120)
self.set_column(default_text_cell, _("Platform"), COL_PLATFORM, 120)
self.set_column(default_text_cell, _("Last Played"), COL_LASTPLAYED_TEXT, 120)
self.set_sort_with_column(COL_LASTPLAYED_TEXT, COL_LASTPLAYED)
self.set_column(default_text_cell, _("Installed At"), COL_INSTALLED_AT_TEXT, 120)
self.set_sort_with_column(COL_INSTALLED_AT_TEXT, COL_INSTALLED_AT)
self.set_column(default_text_cell, _("Play Time"), COL_PLAYTIME_TEXT, 100)
self.set_sort_with_column(COL_PLAYTIME_TEXT, COL_PLAYTIME)
self.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
self.connect_signals()
self.connect("row-activated", self.on_row_activated)
self.get_selection().connect("changed", self.on_cursor_changed)
get_selected_item(self)
¶
Return the currently selected game's id.
Source code in lutris/gui/views/list.py
def get_selected_item(self):
"""Return the currently selected game's id."""
selection = self.get_selection()
if not selection:
return None
_model, select_iter = selection.get_selected()
if select_iter:
return select_iter
on_column_header_button_pressed(self, button, event)
¶
Handles column header button press events
Source code in lutris/gui/views/list.py
def on_column_header_button_pressed(self, button, event):
"""Handles column header button press events"""
if event.button == 3:
menu = GameListColumnToggleMenu(self.get_columns())
menu.popup_at_pointer(None)
return True
on_column_width_changed(col, *args)
staticmethod
¶
Source code in lutris/gui/views/list.py
@staticmethod
def on_column_width_changed(col, *args):
col_name = col.get_title()
if col_name:
settings.write_setting(
col_name.replace(" ", "") + "_column_width",
col.get_fixed_width(),
"list view",
)
on_cursor_changed(self, widget, _line=None, _column=None)
¶
Source code in lutris/gui/views/list.py
def on_cursor_changed(self, widget, _line=None, _column=None):
selected_item = self.get_selected_item()
self.emit("game-selected", selected_item)
on_row_activated(self, widget, line=None, column=None)
¶
Handles double clicks
Source code in lutris/gui/views/list.py
def on_row_activated(self, widget, line=None, column=None):
"""Handles double clicks"""
selected_item = self.get_selected_item()
if selected_item:
selected_id = self.get_selected_id(selected_item)
else:
selected_id = None
self.emit("game-activated", selected_id)
select(self)
¶
Selects the object pointed by current_path
Source code in lutris/gui/views/list.py
def select(self):
self.set_cursor(self.current_path[0])
set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None)
¶
Source code in lutris/gui/views/list.py
def set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None):
column = Gtk.TreeViewColumn(header, cell, markup=column_id)
column.set_sort_indicator(True)
column.set_sort_column_id(column_id if sort_id is None else sort_id)
self.set_column_sort(column_id if sort_id is None else sort_id)
column.set_resizable(True)
column.set_reorderable(True)
width = settings.read_setting("%s_column_width" % COLUMN_NAMES[column_id], "list view")
is_visible = settings.read_setting("%s_visible" % COLUMN_NAMES[column_id], "list view")
column.set_fixed_width(int(width) if width else default_width)
column.set_visible(is_visible == "True" or always_visible if is_visible else True)
self.append_column(column)
column.connect("notify::width", self.on_column_width_changed)
column.get_button().connect('button-press-event', self.on_column_header_button_pressed)
return column
set_column_sort(self, col)
¶
Sort a column and fallback to sorting by name and runner.
Source code in lutris/gui/views/list.py
def set_column_sort(self, col):
"""Sort a column and fallback to sorting by name and runner."""
model = self.get_model()
if model:
model.set_sort_func(col, sort_func, col)
set_selected_game(self, game_id)
¶
Source code in lutris/gui/views/list.py
def set_selected_game(self, game_id):
row = self.game_store.get_row_by_id(game_id, filtered=True)
if row:
self.set_cursor(row.path)
set_sort_with_column(self, col, sort_col)
¶
Sort a column by using another column's data
Source code in lutris/gui/views/list.py
def set_sort_with_column(self, col, sort_col):
"""Sort a column by using another column's data"""
self.model.set_sort_func(col, sort_func, sort_col)
set_text_cell()
staticmethod
¶
Source code in lutris/gui/views/list.py
@staticmethod
def set_text_cell():
text_cell = Gtk.CellRendererText()
text_cell.set_padding(10, 0)
text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
return text_cell
media_loader
¶
Loads game media in parallel
download_media(media_urls, service_media)
¶
Download a list of media files concurrently.
Limits the number of simultaneous downloads to avoid API throttling and UI being overloaded with signals.
Source code in lutris/gui/views/media_loader.py
def download_media(media_urls, service_media):
"""Download a list of media files concurrently.
Limits the number of simultaneous downloads to avoid API throttling
and UI being overloaded with signals.
"""
icons = {}
num_workers = 5
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
future_downloads = {
executor.submit(service_media.download, slug, url): slug
for slug, url in media_urls.items()
if url
}
for future in concurrent.futures.as_completed(future_downloads):
slug = future_downloads[future]
try:
path = future.result()
except Exception as ex: # pylint: disable=broad-except
logger.exception('%r failed: %s', slug, ex)
path = None
if system.path_exists(path):
icons[slug] = path
return icons
store
¶
Store object for a list of games
GameStore (Object)
¶
Source code in lutris/gui/views/store.py
class GameStore(GObject.Object):
__gsignals__ = {
"icons-changed": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, service, service_media):
super().__init__()
self.service = service
self.service_media = service_media
self._installed_games = []
self._installed_games_accessed = False
self._icon_updates = {}
self.store = Gtk.ListStore(
str,
str,
str,
Pixbuf,
str,
str,
str,
str,
int,
str,
bool,
int,
str,
float,
str,
)
@property
def installed_game_slugs(self):
previous_access = self._installed_games_accessed or 0
self._installed_games_accessed = time.time()
if self._installed_games_accessed - previous_access > 1:
self._installed_games = [g["slug"] for g in get_games(filters={"installed": "1"})]
return self._installed_games
def add_games(self, games):
"""Add games to the store"""
for game in list(games):
GLib.idle_add(self.add_game, game)
def get_row_by_slug(self, slug):
for model_row in self.store:
if model_row[COL_SLUG] == slug:
return model_row
def get_row_by_id(self, _id):
if not _id:
return
for model_row in self.store:
try:
if model_row[COL_ID] == str(_id):
return model_row
except TypeError:
return
def remove_game(self, _id):
"""Remove a game from the view."""
row = self.get_row_by_id(_id)
if row:
self.store.remove(row.iter)
def update(self, db_game):
"""Update game informations
Return whether a row was updated; False if the game was not already
present.
"""
store_item = StoreItem(db_game, self.service_media)
row = self.get_row_by_id(store_item.id)
if not row:
row = self.get_row_by_id(db_game["service_id"])
if not row:
return False
row[COL_ID] = str(store_item.id)
row[COL_SLUG] = store_item.slug
row[COL_NAME] = gtk_safe(store_item.name)
if settings.SHOW_MEDIA:
row[COL_ICON] = store_item.get_pixbuf()
else:
row[COL_ICON] = None
row[COL_YEAR] = store_item.year
row[COL_RUNNER] = store_item.runner
row[COL_RUNNER_HUMAN_NAME] = gtk_safe(store_item.runner_text)
row[COL_PLATFORM] = gtk_safe(store_item.platform)
row[COL_LASTPLAYED] = store_item.lastplayed
row[COL_LASTPLAYED_TEXT] = store_item.lastplayed_text
row[COL_INSTALLED] = store_item.installed
row[COL_INSTALLED_AT] = store_item.installed_at
row[COL_INSTALLED_AT_TEXT] = store_item.installed_at_text
row[COL_PLAYTIME] = store_item.playtime
row[COL_PLAYTIME_TEXT] = store_item.playtime_text
return True
def add_game(self, db_game):
"""Add a PGA game to the store"""
game = StoreItem(db_game, self.service_media)
self.store.append(
(
str(game.id),
game.slug,
game.name,
game.get_pixbuf() if settings.SHOW_MEDIA else None,
game.year,
game.runner,
game.runner_text,
gtk_safe(game.platform),
game.lastplayed,
game.lastplayed_text,
game.installed,
game.installed_at,
game.installed_at_text,
game.playtime,
game.playtime_text,
)
)
def on_game_updated(self, game):
if self.service:
db_games = sql.filtered_query(
settings.PGA_DB,
"service_games",
filters=({
"service": self.service_media.service,
"appid": game.appid
})
)
else:
db_games = sql.filtered_query(
settings.PGA_DB,
"games",
filters=({
"id": game.id
})
)
for db_game in db_games:
GLib.idle_add(self.update, db_game)
return True
installed_game_slugs
property
readonly
¶
__init__(self, service, service_media)
special
¶
Source code in lutris/gui/views/store.py
def __init__(self, service, service_media):
super().__init__()
self.service = service
self.service_media = service_media
self._installed_games = []
self._installed_games_accessed = False
self._icon_updates = {}
self.store = Gtk.ListStore(
str,
str,
str,
Pixbuf,
str,
str,
str,
str,
int,
str,
bool,
int,
str,
float,
str,
)
add_game(self, db_game)
¶
Add a PGA game to the store
Source code in lutris/gui/views/store.py
def add_game(self, db_game):
"""Add a PGA game to the store"""
game = StoreItem(db_game, self.service_media)
self.store.append(
(
str(game.id),
game.slug,
game.name,
game.get_pixbuf() if settings.SHOW_MEDIA else None,
game.year,
game.runner,
game.runner_text,
gtk_safe(game.platform),
game.lastplayed,
game.lastplayed_text,
game.installed,
game.installed_at,
game.installed_at_text,
game.playtime,
game.playtime_text,
)
)
add_games(self, games)
¶
Add games to the store
Source code in lutris/gui/views/store.py
def add_games(self, games):
"""Add games to the store"""
for game in list(games):
GLib.idle_add(self.add_game, game)
get_row_by_id(self, _id)
¶
Source code in lutris/gui/views/store.py
def get_row_by_id(self, _id):
if not _id:
return
for model_row in self.store:
try:
if model_row[COL_ID] == str(_id):
return model_row
except TypeError:
return
get_row_by_slug(self, slug)
¶
Source code in lutris/gui/views/store.py
def get_row_by_slug(self, slug):
for model_row in self.store:
if model_row[COL_SLUG] == slug:
return model_row
on_game_updated(self, game)
¶
Source code in lutris/gui/views/store.py
def on_game_updated(self, game):
if self.service:
db_games = sql.filtered_query(
settings.PGA_DB,
"service_games",
filters=({
"service": self.service_media.service,
"appid": game.appid
})
)
else:
db_games = sql.filtered_query(
settings.PGA_DB,
"games",
filters=({
"id": game.id
})
)
for db_game in db_games:
GLib.idle_add(self.update, db_game)
return True
remove_game(self, _id)
¶
Remove a game from the view.
Source code in lutris/gui/views/store.py
def remove_game(self, _id):
"""Remove a game from the view."""
row = self.get_row_by_id(_id)
if row:
self.store.remove(row.iter)
update(self, db_game)
¶
Update game informations Return whether a row was updated; False if the game was not already present.
Source code in lutris/gui/views/store.py
def update(self, db_game):
"""Update game informations
Return whether a row was updated; False if the game was not already
present.
"""
store_item = StoreItem(db_game, self.service_media)
row = self.get_row_by_id(store_item.id)
if not row:
row = self.get_row_by_id(db_game["service_id"])
if not row:
return False
row[COL_ID] = str(store_item.id)
row[COL_SLUG] = store_item.slug
row[COL_NAME] = gtk_safe(store_item.name)
if settings.SHOW_MEDIA:
row[COL_ICON] = store_item.get_pixbuf()
else:
row[COL_ICON] = None
row[COL_YEAR] = store_item.year
row[COL_RUNNER] = store_item.runner
row[COL_RUNNER_HUMAN_NAME] = gtk_safe(store_item.runner_text)
row[COL_PLATFORM] = gtk_safe(store_item.platform)
row[COL_LASTPLAYED] = store_item.lastplayed
row[COL_LASTPLAYED_TEXT] = store_item.lastplayed_text
row[COL_INSTALLED] = store_item.installed
row[COL_INSTALLED_AT] = store_item.installed_at
row[COL_INSTALLED_AT_TEXT] = store_item.installed_at_text
row[COL_PLAYTIME] = store_item.playtime
row[COL_PLAYTIME_TEXT] = store_item.playtime_text
return True
sort_func(model, row1, row2, sort_col)
¶
Sorting function for the game store
Source code in lutris/gui/views/store.py
def sort_func(model, row1, row2, sort_col):
"""Sorting function for the game store"""
value1 = model.get_value(row1, sort_col)
value2 = model.get_value(row2, sort_col)
if value1 is None and value2 is None:
value1 = value2 = 0
elif value1 is None:
value1 = type(value2)()
elif value2 is None:
value2 = type(value1)()
value1 = try_lower(value1)
value2 = try_lower(value2)
diff = -1 if value1 < value2 else 0 if value1 == value2 else 1
if diff == 0:
value1 = try_lower(model.get_value(row1, COL_NAME))
value2 = try_lower(model.get_value(row2, COL_NAME))
try:
diff = -1 if value1 < value2 else 0 if value1 == value2 else 1
except TypeError:
diff = 0
if diff == 0:
value1 = try_lower(model.get_value(row1, COL_RUNNER_HUMAN_NAME))
value2 = try_lower(model.get_value(row2, COL_RUNNER_HUMAN_NAME))
try:
return -1 if value1 < value2 else 0 if value1 == value2 else 1
except TypeError:
return 0
try_lower(value)
¶
Source code in lutris/gui/views/store.py
def try_lower(value):
try:
out = value.lower()
except AttributeError:
out = value
return out
store_item
¶
Game representation for views
StoreItem
¶
Representation of a game for views TODO: Fix overlap with Game class
Source code in lutris/gui/views/store_item.py
class StoreItem:
"""Representation of a game for views
TODO: Fix overlap with Game class
"""
def __init__(self, game_data, service_media):
if not game_data:
raise RuntimeError("No game data provided")
self._game_data = game_data
self.service_media = service_media
def __str__(self):
return self.name
def __repr__(self):
return "<Store id=%s slug=%s>" % (self.id, self.slug)
@property
def id(self): # pylint: disable=invalid-name
"""Game internal ID"""
# Return an unique identifier for the game.
# Since service games are not related to lutris, use the appid
if "service_id" not in self._game_data:
if "appid" in self._game_data:
return self._game_data["appid"]
return self._game_data["slug"]
return self._game_data["id"]
@property
def service(self):
return gtk_safe(self._game_data.get("service"))
@property
def slug(self):
"""Slug identifier"""
return gtk_safe(self._game_data["slug"])
@property
def name(self):
"""Name"""
return gtk_safe(self._game_data["name"])
@property
def year(self):
"""Year"""
return str(self._game_data.get("year") or "")
@property
def runner(self):
"""Runner slug"""
return gtk_safe(self._game_data.get("runner")) or ""
@property
def runner_text(self):
"""Runner name"""
return gtk_safe(RUNNER_NAMES.get(self.runner))
@property
def platform(self):
"""Platform"""
_platform = self._game_data.get("platform")
if not _platform and not self.service and self.installed:
game_inst = Game(self._game_data["id"])
if game_inst.platform:
_platform = game_inst.platform
return gtk_safe(_platform)
@property
def installed(self):
"""Game is installed"""
if "service_id" not in self._game_data:
return self.id in get_service_games(self.service)
if not self._game_data.get("runner"):
return False
return self._game_data.get("installed")
def get_pixbuf(self):
"""Pixbuf varying on icon type"""
if self._game_data.get("icon"):
image_path = self._game_data["icon"]
else:
image_path = self.service_media.get_absolute_path(self.slug)
if not system.path_exists(image_path):
service = self._game_data.get("service")
appid = self._game_data.get("service_id")
if appid:
service_game = ServiceGameCollection.get_game(service, appid)
else:
service_game = None
if service_game:
image_path = self.service_media.get_absolute_path(service_game["slug"])
if system.path_exists(image_path):
return get_pixbuf(image_path, self.service_media.size, is_installed=self.installed)
return self.service_media.get_pixbuf_for_game(
self._game_data["slug"],
self.installed
)
@property
def installed_at(self):
"""Date of install"""
return self._game_data.get("installed_at")
@property
def installed_at_text(self):
"""Date of install (textual representation)"""
return gtk_safe(
time.strftime("%X %x", time.localtime(self.installed_at)) if
self.installed_at else ""
)
@property
def lastplayed(self):
"""Date of last play"""
return self._game_data.get("lastplayed")
@property
def lastplayed_text(self):
"""Date of last play (textual representation)"""
return gtk_safe(
time.strftime(
"%X %x",
time.localtime(self.lastplayed)
) if self.lastplayed else ""
)
@property
def playtime(self):
"""Playtime duration in hours"""
try:
return float(self._game_data.get("playtime", 0))
except (TypeError, ValueError):
return 0.0
@property
def playtime_text(self):
"""Playtime duration in hours (textual representation)"""
try:
_playtime_text = get_formatted_playtime(self.playtime)
except ValueError:
logger.warning("Invalid playtime value %s for %s", self.playtime, self)
_playtime_text = "" # Do not show erroneous values
return gtk_safe(_playtime_text)
id
property
readonly
¶
Game internal ID
installed
property
readonly
¶
Game is installed
installed_at
property
readonly
¶
Date of install
installed_at_text
property
readonly
¶
Date of install (textual representation)
lastplayed
property
readonly
¶
Date of last play
lastplayed_text
property
readonly
¶
Date of last play (textual representation)
name
property
readonly
¶
Name
platform
property
readonly
¶
Platform
playtime
property
readonly
¶
Playtime duration in hours
playtime_text
property
readonly
¶
Playtime duration in hours (textual representation)
runner
property
readonly
¶
Runner slug
runner_text
property
readonly
¶
Runner name
service
property
readonly
¶
slug
property
readonly
¶
Slug identifier
year
property
readonly
¶
Year
__init__(self, game_data, service_media)
special
¶
Source code in lutris/gui/views/store_item.py
def __init__(self, game_data, service_media):
if not game_data:
raise RuntimeError("No game data provided")
self._game_data = game_data
self.service_media = service_media
__repr__(self)
special
¶
Source code in lutris/gui/views/store_item.py
def __repr__(self):
return "<Store id=%s slug=%s>" % (self.id, self.slug)
__str__(self)
special
¶
Source code in lutris/gui/views/store_item.py
def __str__(self):
return self.name
get_pixbuf(self)
¶
Pixbuf varying on icon type
Source code in lutris/gui/views/store_item.py
def get_pixbuf(self):
"""Pixbuf varying on icon type"""
if self._game_data.get("icon"):
image_path = self._game_data["icon"]
else:
image_path = self.service_media.get_absolute_path(self.slug)
if not system.path_exists(image_path):
service = self._game_data.get("service")
appid = self._game_data.get("service_id")
if appid:
service_game = ServiceGameCollection.get_game(service, appid)
else:
service_game = None
if service_game:
image_path = self.service_media.get_absolute_path(service_game["slug"])
if system.path_exists(image_path):
return get_pixbuf(image_path, self.service_media.size, is_installed=self.installed)
return self.service_media.get_pixbuf_for_game(
self._game_data["slug"],
self.installed
)
widgets
special
¶
cellrenderers
¶
GridViewCellRendererText (CellRendererText)
¶
CellRendererText adjusted for grid view display, removes extra padding
Source code in lutris/gui/widgets/cellrenderers.py
class GridViewCellRendererText(Gtk.CellRendererText):
"""CellRendererText adjusted for grid view display, removes extra padding"""
def __init__(self, width, *args, **kwargs):
super().__init__(*args, **kwargs)
self.props.alignment = Pango.Alignment.CENTER
self.props.wrap_mode = Pango.WrapMode.WORD
self.props.xalign = 0.5
self.props.yalign = 0
self.props.wrap_width = width
__init__(self, width, *args, **kwargs)
special
¶
Source code in lutris/gui/widgets/cellrenderers.py
def __init__(self, width, *args, **kwargs):
super().__init__(*args, **kwargs)
self.props.alignment = Pango.Alignment.CENTER
self.props.wrap_mode = Pango.WrapMode.WORD
self.props.xalign = 0.5
self.props.yalign = 0
self.props.wrap_width = width
common
¶
Misc widgets used in the GUI.
EditableGrid (Grid)
¶
Source code in lutris/gui/widgets/common.py
class EditableGrid(Gtk.Grid):
__gsignals__ = {"changed": (GObject.SIGNAL_RUN_FIRST, None, ())}
def __init__(self, data, columns):
self.columns = columns
super().__init__()
self.set_column_homogeneous(True)
self.set_row_homogeneous(True)
self.set_row_spacing(10)
self.set_column_spacing(10)
self.liststore = Gtk.ListStore(str, str)
for item in data:
self.liststore.append([str(value) for value in item])
self.treeview = Gtk.TreeView.new_with_model(self.liststore)
self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
for i, column_title in enumerate(self.columns):
renderer = Gtk.CellRendererText()
renderer.set_property("editable", True)
renderer.connect("edited", self.on_text_edited, i)
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_resizable(True)
column.set_min_width(100)
column.set_sort_column_id(0)
self.treeview.append_column(column)
self.buttons = []
self.add_button = Gtk.Button(_("Add"))
self.buttons.append(self.add_button)
self.add_button.connect("clicked", self.on_add)
self.delete_button = Gtk.Button(_("Delete"))
self.buttons.append(self.delete_button)
self.delete_button.connect("clicked", self.on_delete)
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.scrollable_treelist.add(self.treeview)
self.attach(self.scrollable_treelist, 0, 0, 5, 5)
self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)
for i, button in enumerate(self.buttons[1:]):
self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
self.show_all()
def on_add(self, widget): # pylint: disable=unused-argument
self.liststore.append(["", ""])
row_position = len(self.liststore) - 1
self.treeview.set_cursor(row_position, None, False)
self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)
self.emit("changed")
def on_delete(self, widget): # pylint: disable=unused-argument
selection = self.treeview.get_selection()
_, iteration = selection.get_selected()
self.liststore.remove(iteration)
self.emit("changed")
def on_text_edited(self, widget, path, text, field): # pylint: disable=unused-argument
self.liststore[path][field] = text.strip() # pylint: disable=unsubscriptable-object
self.emit("changed")
def get_data(self): # pylint: disable=arguments-differ
model_data = []
for row in self.liststore: # pylint: disable=not-an-iterable
model_data.append(row)
return model_data
__init__(self, data, columns)
special
¶
Source code in lutris/gui/widgets/common.py
def __init__(self, data, columns):
self.columns = columns
super().__init__()
self.set_column_homogeneous(True)
self.set_row_homogeneous(True)
self.set_row_spacing(10)
self.set_column_spacing(10)
self.liststore = Gtk.ListStore(str, str)
for item in data:
self.liststore.append([str(value) for value in item])
self.treeview = Gtk.TreeView.new_with_model(self.liststore)
self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
for i, column_title in enumerate(self.columns):
renderer = Gtk.CellRendererText()
renderer.set_property("editable", True)
renderer.connect("edited", self.on_text_edited, i)
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_resizable(True)
column.set_min_width(100)
column.set_sort_column_id(0)
self.treeview.append_column(column)
self.buttons = []
self.add_button = Gtk.Button(_("Add"))
self.buttons.append(self.add_button)
self.add_button.connect("clicked", self.on_add)
self.delete_button = Gtk.Button(_("Delete"))
self.buttons.append(self.delete_button)
self.delete_button.connect("clicked", self.on_delete)
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.scrollable_treelist.add(self.treeview)
self.attach(self.scrollable_treelist, 0, 0, 5, 5)
self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)
for i, button in enumerate(self.buttons[1:]):
self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
self.show_all()
get_data(self)
¶
get_data(self, key:str)
Source code in lutris/gui/widgets/common.py
def get_data(self): # pylint: disable=arguments-differ
model_data = []
for row in self.liststore: # pylint: disable=not-an-iterable
model_data.append(row)
return model_data
on_add(self, widget)
¶
Source code in lutris/gui/widgets/common.py
def on_add(self, widget): # pylint: disable=unused-argument
self.liststore.append(["", ""])
row_position = len(self.liststore) - 1
self.treeview.set_cursor(row_position, None, False)
self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)
self.emit("changed")
on_delete(self, widget)
¶
Source code in lutris/gui/widgets/common.py
def on_delete(self, widget): # pylint: disable=unused-argument
selection = self.treeview.get_selection()
_, iteration = selection.get_selected()
self.liststore.remove(iteration)
self.emit("changed")
on_text_edited(self, widget, path, text, field)
¶
Source code in lutris/gui/widgets/common.py
def on_text_edited(self, widget, path, text, field): # pylint: disable=unused-argument
self.liststore[path][field] = text.strip() # pylint: disable=unsubscriptable-object
self.emit("changed")
FileChooserEntry (Box)
¶
Editable entry with a file picker button
Source code in lutris/gui/widgets/common.py
class FileChooserEntry(Gtk.Box):
"""Editable entry with a file picker button"""
max_completion_items = 15 # Maximum number of items to display in the autocompletion dropdown.
def __init__(
self,
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=None,
default_path=None,
warn_if_non_empty=False,
warn_if_ntfs=False
):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
visible=True
)
self.title = title
self.action = action
self.path = os.path.expanduser(path) if path else None
self.default_path = os.path.expanduser(default_path) if default_path else path
self.warn_if_non_empty = warn_if_non_empty
self.warn_if_ntfs = warn_if_ntfs
self.path_completion = Gtk.ListStore(str)
self.entry = Gtk.Entry(visible=True)
self.entry.set_completion(self.get_completion())
self.entry.connect("changed", self.on_entry_changed)
if path:
self.entry.set_text(path)
browse_button = Gtk.Button(_("Browse..."), visible=True)
browse_button.connect("clicked", self.on_browse_clicked)
box = Gtk.Box(spacing=6, visible=True)
box.pack_start(self.entry, True, True, 0)
box.add(browse_button)
self.pack_start(box, False, False, 0)
def get_text(self):
"""Return the entry's text"""
return self.entry.get_text()
def get_filename(self):
"""Deprecated"""
logger.warning("Just use get_text")
return self.get_text()
def get_completion(self):
"""Return an EntryCompletion widget"""
completion = Gtk.EntryCompletion()
completion.set_model(self.path_completion)
completion.set_text_column(0)
return completion
def get_filechooser_dialog(self):
"""Return an instance of a FileChooserNative configured for this widget"""
dialog = Gtk.FileChooserNative.new(self.title, None, self.action, _("_OK"), _("_Cancel"))
dialog.set_create_folders(True)
dialog.set_current_folder(self.get_default_folder())
dialog.connect("response", self.on_select_file, dialog)
return dialog
def get_default_folder(self):
"""Return the default folder for the file picker"""
default_path = self.path or self.default_path or ""
if not default_path or not system.path_exists(default_path):
current_entry = self.get_text()
if system.path_exists(current_entry):
default_path = current_entry
if not os.path.isdir(default_path):
default_path = os.path.dirname(default_path)
return os.path.expanduser(default_path or "~")
def on_browse_clicked(self, _widget):
"""Browse button click callback"""
file_chooser_dialog = self.get_filechooser_dialog()
file_chooser_dialog.show()
def on_entry_changed(self, widget):
"""Entry changed callback"""
self.clear_warnings()
path = widget.get_text()
if not path:
return
path = os.path.expanduser(path)
self.update_completion(path)
self.path = path
if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
ntfs_box = Gtk.Box(spacing=6, visible=True)
warning_image = Gtk.Image(visible=True)
warning_image.set_from_pixbuf(get_stock_icon("dialog-warning", 32))
ntfs_box.add(warning_image)
ntfs_label = Gtk.Label(visible=True)
ntfs_label.set_markup(_(
"<b>Warning!</b> The selected path is located on a drive formatted by Windows.\n"
"Games and programs installed on Windows drives usually <b>don't work</b>."
))
ntfs_box.add(ntfs_label)
self.pack_end(ntfs_box, False, False, 10)
if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):
non_empty_label = Gtk.Label(visible=True)
non_empty_label.set_markup(_(
"<b>Warning!</b> The selected path "
"contains files. Installation might not work properly."
))
self.pack_end(non_empty_label, False, False, 10)
parent = system.get_existing_parent(path)
if parent is not None and not os.access(parent, os.W_OK):
non_writable_destination_label = Gtk.Label(visible=True)
non_writable_destination_label.set_markup(_(
"<b>Warning</b> The destination folder "
"is not writable by the current user."
))
self.pack_end(non_writable_destination_label, False, False, 10)
def on_select_file(self, dialog, response, _dialog):
"""FileChooserDialog response callback"""
if response == Gtk.ResponseType.ACCEPT:
target_path = dialog.get_filename()
if target_path:
dialog.set_current_folder(target_path)
self.entry.set_text(system.reverse_expanduser(target_path))
dialog.destroy()
def update_completion(self, current_path):
"""Update the auto-completion widget with the current path"""
self.path_completion.clear()
if not os.path.exists(current_path):
current_path, filefilter = os.path.split(current_path)
else:
filefilter = None
if os.path.isdir(current_path):
index = 0
for filename in sorted(os.listdir(current_path)):
if filename.startswith("."):
continue
if filefilter is not None and not filename.startswith(filefilter):
continue
self.path_completion.append([os.path.join(current_path, filename)])
index += 1
if index > self.max_completion_items:
break
def clear_warnings(self):
"""Delete all the warning labels from the container"""
for index, child in enumerate(self.get_children()):
if index > 0:
child.destroy()
max_completion_items
¶
__init__(self, title='Select file', action=<enum GTK_FILE_CHOOSER_ACTION_OPEN of type Gtk.FileChooserAction>, path=None, default_path=None, warn_if_non_empty=False, warn_if_ntfs=False)
special
¶
Source code in lutris/gui/widgets/common.py
def __init__(
self,
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=None,
default_path=None,
warn_if_non_empty=False,
warn_if_ntfs=False
):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
visible=True
)
self.title = title
self.action = action
self.path = os.path.expanduser(path) if path else None
self.default_path = os.path.expanduser(default_path) if default_path else path
self.warn_if_non_empty = warn_if_non_empty
self.warn_if_ntfs = warn_if_ntfs
self.path_completion = Gtk.ListStore(str)
self.entry = Gtk.Entry(visible=True)
self.entry.set_completion(self.get_completion())
self.entry.connect("changed", self.on_entry_changed)
if path:
self.entry.set_text(path)
browse_button = Gtk.Button(_("Browse..."), visible=True)
browse_button.connect("clicked", self.on_browse_clicked)
box = Gtk.Box(spacing=6, visible=True)
box.pack_start(self.entry, True, True, 0)
box.add(browse_button)
self.pack_start(box, False, False, 0)
clear_warnings(self)
¶
Delete all the warning labels from the container
Source code in lutris/gui/widgets/common.py
def clear_warnings(self):
"""Delete all the warning labels from the container"""
for index, child in enumerate(self.get_children()):
if index > 0:
child.destroy()
get_completion(self)
¶
Return an EntryCompletion widget
Source code in lutris/gui/widgets/common.py
def get_completion(self):
"""Return an EntryCompletion widget"""
completion = Gtk.EntryCompletion()
completion.set_model(self.path_completion)
completion.set_text_column(0)
return completion
get_default_folder(self)
¶
Return the default folder for the file picker
Source code in lutris/gui/widgets/common.py
def get_default_folder(self):
"""Return the default folder for the file picker"""
default_path = self.path or self.default_path or ""
if not default_path or not system.path_exists(default_path):
current_entry = self.get_text()
if system.path_exists(current_entry):
default_path = current_entry
if not os.path.isdir(default_path):
default_path = os.path.dirname(default_path)
return os.path.expanduser(default_path or "~")
get_filechooser_dialog(self)
¶
Return an instance of a FileChooserNative configured for this widget
Source code in lutris/gui/widgets/common.py
def get_filechooser_dialog(self):
"""Return an instance of a FileChooserNative configured for this widget"""
dialog = Gtk.FileChooserNative.new(self.title, None, self.action, _("_OK"), _("_Cancel"))
dialog.set_create_folders(True)
dialog.set_current_folder(self.get_default_folder())
dialog.connect("response", self.on_select_file, dialog)
return dialog
get_filename(self)
¶
Deprecated
Source code in lutris/gui/widgets/common.py
def get_filename(self):
"""Deprecated"""
logger.warning("Just use get_text")
return self.get_text()
get_text(self)
¶
Return the entry's text
Source code in lutris/gui/widgets/common.py
def get_text(self):
"""Return the entry's text"""
return self.entry.get_text()
on_browse_clicked(self, _widget)
¶
Browse button click callback
Source code in lutris/gui/widgets/common.py
def on_browse_clicked(self, _widget):
"""Browse button click callback"""
file_chooser_dialog = self.get_filechooser_dialog()
file_chooser_dialog.show()
on_entry_changed(self, widget)
¶
Entry changed callback
Source code in lutris/gui/widgets/common.py
def on_entry_changed(self, widget):
"""Entry changed callback"""
self.clear_warnings()
path = widget.get_text()
if not path:
return
path = os.path.expanduser(path)
self.update_completion(path)
self.path = path
if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
ntfs_box = Gtk.Box(spacing=6, visible=True)
warning_image = Gtk.Image(visible=True)
warning_image.set_from_pixbuf(get_stock_icon("dialog-warning", 32))
ntfs_box.add(warning_image)
ntfs_label = Gtk.Label(visible=True)
ntfs_label.set_markup(_(
"<b>Warning!</b> The selected path is located on a drive formatted by Windows.\n"
"Games and programs installed on Windows drives usually <b>don't work</b>."
))
ntfs_box.add(ntfs_label)
self.pack_end(ntfs_box, False, False, 10)
if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):
non_empty_label = Gtk.Label(visible=True)
non_empty_label.set_markup(_(
"<b>Warning!</b> The selected path "
"contains files. Installation might not work properly."
))
self.pack_end(non_empty_label, False, False, 10)
parent = system.get_existing_parent(path)
if parent is not None and not os.access(parent, os.W_OK):
non_writable_destination_label = Gtk.Label(visible=True)
non_writable_destination_label.set_markup(_(
"<b>Warning</b> The destination folder "
"is not writable by the current user."
))
self.pack_end(non_writable_destination_label, False, False, 10)
on_select_file(self, dialog, response, _dialog)
¶
FileChooserDialog response callback
Source code in lutris/gui/widgets/common.py
def on_select_file(self, dialog, response, _dialog):
"""FileChooserDialog response callback"""
if response == Gtk.ResponseType.ACCEPT:
target_path = dialog.get_filename()
if target_path:
dialog.set_current_folder(target_path)
self.entry.set_text(system.reverse_expanduser(target_path))
dialog.destroy()
update_completion(self, current_path)
¶
Update the auto-completion widget with the current path
Source code in lutris/gui/widgets/common.py
def update_completion(self, current_path):
"""Update the auto-completion widget with the current path"""
self.path_completion.clear()
if not os.path.exists(current_path):
current_path, filefilter = os.path.split(current_path)
else:
filefilter = None
if os.path.isdir(current_path):
index = 0
for filename in sorted(os.listdir(current_path)):
if filename.startswith("."):
continue
if filefilter is not None and not filename.startswith(filefilter):
continue
self.path_completion.append([os.path.join(current_path, filename)])
index += 1
if index > self.max_completion_items:
break
InstallerLabel (Label)
¶
Label for installer window
Source code in lutris/gui/widgets/common.py
class InstallerLabel(Gtk.Label):
"""Label for installer window"""
def __init__(self, message=None):
super().__init__(label=message)
self.set_max_width_chars(80)
self.set_property("wrap", True)
self.set_use_markup(True)
self.set_selectable(True)
self.set_alignment(0.5, 0)
__init__(self, message=None)
special
¶
Source code in lutris/gui/widgets/common.py
def __init__(self, message=None):
super().__init__(label=message)
self.set_max_width_chars(80)
self.set_property("wrap", True)
self.set_use_markup(True)
self.set_selectable(True)
self.set_alignment(0.5, 0)
Label (Label)
¶
Standardised label for config vboxes.
Source code in lutris/gui/widgets/common.py
class Label(Gtk.Label):
"""Standardised label for config vboxes."""
def __init__(self, message=None):
"""Custom init of label."""
super().__init__(label=message)
self.set_line_wrap(True)
self.set_max_width_chars(22)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
self.set_size_request(230, -1)
self.set_alignment(0, 0.5)
self.set_justify(Gtk.Justification.LEFT)
__init__(self, message=None)
special
¶
Custom init of label.
Source code in lutris/gui/widgets/common.py
def __init__(self, message=None):
"""Custom init of label."""
super().__init__(label=message)
self.set_line_wrap(True)
self.set_max_width_chars(22)
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
self.set_size_request(230, -1)
self.set_alignment(0, 0.5)
self.set_justify(Gtk.Justification.LEFT)
NumberEntry (Entry, Editable)
¶
Source code in lutris/gui/widgets/common.py
class NumberEntry(Gtk.Entry, Gtk.Editable):
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept numbers"""
new_text = "".join([c for c in new_text if c.isnumeric()])
if new_text:
self.get_buffer().insert_text(position, new_text, length)
return position + length
return position
do_insert_text(self, new_text, length, position)
¶
Filter inserted characters to only accept numbers
Source code in lutris/gui/widgets/common.py
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept numbers"""
new_text = "".join([c for c in new_text if c.isnumeric()])
if new_text:
self.get_buffer().insert_text(position, new_text, length)
return position + length
return position
SlugEntry (Entry, Editable)
¶
Source code in lutris/gui/widgets/common.py
class SlugEntry(Gtk.Entry, Gtk.Editable):
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept alphanumeric and dashes"""
new_text = "".join([c for c in new_text if c.isalnum() or c == "-"]).lower()
length = len(new_text)
self.get_buffer().insert_text(position, new_text, length)
return position + length
do_insert_text(self, new_text, length, position)
¶
Filter inserted characters to only accept alphanumeric and dashes
Source code in lutris/gui/widgets/common.py
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept alphanumeric and dashes"""
new_text = "".join([c for c in new_text if c.isalnum() or c == "-"]).lower()
length = len(new_text)
self.get_buffer().insert_text(position, new_text, length)
return position + length
VBox (Box)
¶
Source code in lutris/gui/widgets/common.py
class VBox(Gtk.Box):
def __init__(self, **kwargs):
super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)
__init__(self, **kwargs)
special
¶
Source code in lutris/gui/widgets/common.py
def __init__(self, **kwargs):
super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)
contextual_menu
¶
ContextualMenu (Menu)
¶
Source code in lutris/gui/widgets/contextual_menu.py
class ContextualMenu(Gtk.Menu):
def __init__(self, main_entries):
super().__init__()
self.main_entries = main_entries
def add_menuitem(self, entry):
"""Add a menu item to the current menu
Params:
entry (tuple): tuple containing name, label and callback
Returns:
Gtk.MenuItem
"""
name, label, callback = entry
action = Gtk.Action(name=name, label=label)
action.connect("activate", callback)
menu_item = action.create_menu_item()
menu_item.action_id = name
self.append(menu_item)
return menu_item
def get_runner_entries(self, game):
if not game:
return None
try:
runner = runners.import_runner(game.runner_name)(game.config)
except runners.InvalidRunner:
return None
return runner.context_menu_entries
def popup(self, event, game_actions, game=None, service=None):
for item in self.get_children():
self.remove(item)
for entry in self.main_entries:
self.add_menuitem(entry)
if game_actions.game.runner_name and game_actions.game.is_installed:
runner_entries = self.get_runner_entries(game)
if runner_entries:
self.append(Gtk.SeparatorMenuItem())
for entry in runner_entries:
self.add_menuitem(entry)
self.show_all()
displayed = game_actions.get_displayed_entries()
for menuitem in self.get_children():
if not isinstance(menuitem, Gtk.ImageMenuItem):
continue
menuitem.set_visible(displayed.get(menuitem.action_id, True))
super().popup_at_pointer(event)
__init__(self, main_entries)
special
¶
Source code in lutris/gui/widgets/contextual_menu.py
def __init__(self, main_entries):
super().__init__()
self.main_entries = main_entries
add_menuitem(self, entry)
¶
Add a menu item to the current menu
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry |
tuple |
tuple containing name, label and callback |
required |
Returns:
| Type | Description |
|---|---|
Gtk.MenuItem |
Source code in lutris/gui/widgets/contextual_menu.py
def add_menuitem(self, entry):
"""Add a menu item to the current menu
Params:
entry (tuple): tuple containing name, label and callback
Returns:
Gtk.MenuItem
"""
name, label, callback = entry
action = Gtk.Action(name=name, label=label)
action.connect("activate", callback)
menu_item = action.create_menu_item()
menu_item.action_id = name
self.append(menu_item)
return menu_item
get_runner_entries(self, game)
¶
Source code in lutris/gui/widgets/contextual_menu.py
def get_runner_entries(self, game):
if not game:
return None
try:
runner = runners.import_runner(game.runner_name)(game.config)
except runners.InvalidRunner:
return None
return runner.context_menu_entries
popup(self, event, game_actions, game=None, service=None)
¶
popup(self, parent_menu_shell:Gtk.Widget=None, parent_menu_item:Gtk.Widget=None, func:Gtk.MenuPositionFunc=None, data=None, button:int, activate_time:int)
Source code in lutris/gui/widgets/contextual_menu.py
def popup(self, event, game_actions, game=None, service=None):
for item in self.get_children():
self.remove(item)
for entry in self.main_entries:
self.add_menuitem(entry)
if game_actions.game.runner_name and game_actions.game.is_installed:
runner_entries = self.get_runner_entries(game)
if runner_entries:
self.append(Gtk.SeparatorMenuItem())
for entry in runner_entries:
self.add_menuitem(entry)
self.show_all()
displayed = game_actions.get_displayed_entries()
for menuitem in self.get_children():
if not isinstance(menuitem, Gtk.ImageMenuItem):
continue
menuitem.set_visible(displayed.get(menuitem.action_id, True))
super().popup_at_pointer(event)
download_progress_box
¶
DownloadProgressBox (Box)
¶
Progress bar used to monitor a file download.
Source code in lutris/gui/widgets/download_progress_box.py
class DownloadProgressBox(Gtk.Box):
"""Progress bar used to monitor a file download."""
__gsignals__ = {
"complete": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
"cancel": (GObject.SignalFlags.RUN_LAST, None, ()),
"error": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
}
def __init__(self, params, cancelable=True, downloader=None):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.downloader = downloader
self.is_complete = False
self.url = params.get("url")
self.dest = params.get("dest")
self.referer = params.get("referer")
self.main_label = Gtk.Label(self.get_title())
self.main_label.set_alignment(0, 0)
self.main_label.set_property("wrap", True)
self.main_label.set_margin_bottom(10)
# self.main_label.set_max_width_chars(70)
self.main_label.set_selectable(True)
self.main_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.pack_start(self.main_label, True, True, 0)
progress_box = Gtk.Box()
self.progressbar = Gtk.ProgressBar()
self.progressbar.set_margin_top(5)
self.progressbar.set_margin_bottom(5)
self.progressbar.set_margin_right(10)
progress_box.pack_start(self.progressbar, True, True, 0)
self.cancel_button = Gtk.Button.new_with_mnemonic(_("_Cancel"))
self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_cancel_clicked)
if not cancelable:
self.cancel_button.set_sensitive(False)
progress_box.pack_end(self.cancel_button, False, False, 0)
self.pack_start(progress_box, False, False, 0)
self.progress_label = Gtk.Label()
self.progress_label.set_alignment(0, 0)
self.pack_start(self.progress_label, True, True, 0)
self.show_all()
self.cancel_button.hide()
def get_title(self):
"""Return the main label text for the widget"""
parsed = urlparse(self.url)
return "%s%s" % (parsed.netloc, parsed.path)
def start(self):
"""Start downloading a file."""
if not self.downloader:
try:
self.downloader = Downloader(self.url, self.dest, referer=self.referer, overwrite=True)
except RuntimeError as ex:
from lutris.gui.dialogs import ErrorDialog
ErrorDialog(ex.args[0])
self.emit("cancel")
return None
timer_id = GLib.timeout_add(500, self._progress)
self.cancel_button.show()
self.cancel_button.set_sensitive(True)
if not self.downloader.state == self.downloader.DOWNLOADING:
self.downloader.start()
return timer_id
def set_retry_button(self):
"""Transform the cancel button into a retry button"""
self.cancel_button.set_label(_("Retry"))
self.cancel_button.disconnect(self.cancel_cb_id)
self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_retry_clicked)
self.cancel_button.set_sensitive(True)
def on_retry_clicked(self, button):
logger.debug("Retrying download")
button.set_label(_("Cancel"))
button.disconnect(self.cancel_cb_id)
self.cancel_cb_id = button.connect("clicked", self.on_cancel_clicked)
self.downloader.reset()
self.start()
def on_cancel_clicked(self, _widget=None):
"""Cancel the current download."""
logger.debug("Download cancel requested")
if self.downloader:
self.downloader.cancel()
self.cancel_button.set_sensitive(False)
self.emit("cancel")
def _progress(self):
"""Show download progress."""
progress = min(self.downloader.check_progress(), 1)
if self.downloader.state in [self.downloader.CANCELLED, self.downloader.ERROR]:
self.progressbar.set_fraction(0)
if self.downloader.state == self.downloader.CANCELLED:
self._set_text(_("Download interrupted"))
self.emit("cancel")
else:
self._set_text(str(self.downloader.error)[:80])
return False
self.progressbar.set_fraction(progress)
megabytes = 1024 * 1024
progress_text = _(
"{downloaded:0.2f} / {size:0.2f}MB ({speed:0.2f}MB/s), {time} remaining"
).format(
downloaded=float(self.downloader.downloaded_size) / megabytes,
size=float(self.downloader.full_size) / megabytes,
speed=float(self.downloader.average_speed) / megabytes,
time=self.downloader.time_left,
)
self._set_text(progress_text)
if self.downloader.state == self.downloader.COMPLETED:
self.cancel_button.set_sensitive(False)
self.is_complete = True
self.emit("complete", {})
return False
return True
def _set_text(self, text):
markup = "<span size='10000'>{}</span>".format(gtk_safe(text))
self.progress_label.set_markup(markup)
__init__(self, params, cancelable=True, downloader=None)
special
¶
Source code in lutris/gui/widgets/download_progress_box.py
def __init__(self, params, cancelable=True, downloader=None):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.downloader = downloader
self.is_complete = False
self.url = params.get("url")
self.dest = params.get("dest")
self.referer = params.get("referer")
self.main_label = Gtk.Label(self.get_title())
self.main_label.set_alignment(0, 0)
self.main_label.set_property("wrap", True)
self.main_label.set_margin_bottom(10)
# self.main_label.set_max_width_chars(70)
self.main_label.set_selectable(True)
self.main_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
self.pack_start(self.main_label, True, True, 0)
progress_box = Gtk.Box()
self.progressbar = Gtk.ProgressBar()
self.progressbar.set_margin_top(5)
self.progressbar.set_margin_bottom(5)
self.progressbar.set_margin_right(10)
progress_box.pack_start(self.progressbar, True, True, 0)
self.cancel_button = Gtk.Button.new_with_mnemonic(_("_Cancel"))
self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_cancel_clicked)
if not cancelable:
self.cancel_button.set_sensitive(False)
progress_box.pack_end(self.cancel_button, False, False, 0)
self.pack_start(progress_box, False, False, 0)
self.progress_label = Gtk.Label()
self.progress_label.set_alignment(0, 0)
self.pack_start(self.progress_label, True, True, 0)
self.show_all()
self.cancel_button.hide()
get_title(self)
¶
Return the main label text for the widget
Source code in lutris/gui/widgets/download_progress_box.py
def get_title(self):
"""Return the main label text for the widget"""
parsed = urlparse(self.url)
return "%s%s" % (parsed.netloc, parsed.path)
on_cancel_clicked(self, _widget=None)
¶
Cancel the current download.
Source code in lutris/gui/widgets/download_progress_box.py
def on_cancel_clicked(self, _widget=None):
"""Cancel the current download."""
logger.debug("Download cancel requested")
if self.downloader:
self.downloader.cancel()
self.cancel_button.set_sensitive(False)
self.emit("cancel")
on_retry_clicked(self, button)
¶
Source code in lutris/gui/widgets/download_progress_box.py
def on_retry_clicked(self, button):
logger.debug("Retrying download")
button.set_label(_("Cancel"))
button.disconnect(self.cancel_cb_id)
self.cancel_cb_id = button.connect("clicked", self.on_cancel_clicked)
self.downloader.reset()
self.start()
set_retry_button(self)
¶
Transform the cancel button into a retry button
Source code in lutris/gui/widgets/download_progress_box.py
def set_retry_button(self):
"""Transform the cancel button into a retry button"""
self.cancel_button.set_label(_("Retry"))
self.cancel_button.disconnect(self.cancel_cb_id)
self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_retry_clicked)
self.cancel_button.set_sensitive(True)
start(self)
¶
Start downloading a file.
Source code in lutris/gui/widgets/download_progress_box.py
def start(self):
"""Start downloading a file."""
if not self.downloader:
try:
self.downloader = Downloader(self.url, self.dest, referer=self.referer, overwrite=True)
except RuntimeError as ex:
from lutris.gui.dialogs import ErrorDialog
ErrorDialog(ex.args[0])
self.emit("cancel")
return None
timer_id = GLib.timeout_add(500, self._progress)
self.cancel_button.show()
self.cancel_button.set_sensitive(True)
if not self.downloader.state == self.downloader.DOWNLOADING:
self.downloader.start()
return timer_id
game_bar
¶
GameBar (Box)
¶
Source code in lutris/gui/widgets/game_bar.py
class GameBar(Gtk.Box):
def __init__(self, db_game, game_actions, application):
"""Create the game bar with a database row"""
super().__init__(orientation=Gtk.Orientation.VERTICAL, visible=True,
margin_top=12,
margin_left=12,
margin_bottom=12,
margin_right=12,
spacing=6)
self.game_start_hook_id = GObject.add_emission_hook(Game, "game-start", self.on_game_state_changed)
self.game_started_hook_id = GObject.add_emission_hook(Game, "game-started", self.on_game_state_changed)
self.game_stopped_hook_id = GObject.add_emission_hook(Game, "game-stopped", self.on_game_state_changed)
self.game_updated_hook_id = GObject.add_emission_hook(Game, "game-updated", self.on_game_state_changed)
self.game_removed_hook_id = GObject.add_emission_hook(Game, "game-removed", self.on_game_state_changed)
self.game_installed_hook_id = GObject.add_emission_hook(Game, "game-installed", self.on_game_state_changed)
self.connect("destroy", self.on_destroy)
self.set_margin_bottom(12)
self.game_actions = game_actions
self.db_game = db_game
self.service = None
if db_game.get("service"):
try:
self.service = services.SERVICES[db_game["service"]]()
except KeyError:
pass
game_id = None
if "service_id" in db_game:
self.appid = db_game["service_id"]
game_id = db_game["id"]
elif self.service:
self.appid = db_game["appid"]
if self.service.id == "lutris":
game = get_game_by_field(self.appid, field="slug")
else:
game = get_game_for_service(self.service.id, self.appid)
if game:
game_id = game["id"]
if game_id:
self.game = application.get_game_by_id(game_id) or Game(game_id)
else:
self.game = Game()
self.game.name = db_game["name"]
self.game.slug = db_game["slug"]
self.game.appid = self.appid
self.game.service = self.service.id if self.service else None
game_actions.set_game(self.game)
self.update_view()
def on_destroy(self, widget):
GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)
GObject.remove_emission_hook(Game, "game-started", self.game_started_hook_id)
GObject.remove_emission_hook(Game, "game-stopped", self.game_stopped_hook_id)
GObject.remove_emission_hook(Game, "game-updated", self.game_updated_hook_id)
GObject.remove_emission_hook(Game, "game-removed", self.game_removed_hook_id)
GObject.remove_emission_hook(Game, "game-installed", self.game_installed_hook_id)
return True
def clear_view(self):
"""Clears all widgets from the container"""
for child in self.get_children():
child.destroy()
def update_view(self):
"""Populate the view with widgets"""
game_label = self.get_game_name_label()
game_label.set_halign(Gtk.Align.START)
self.pack_start(game_label, False, False, 0)
hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, spacing=6)
self.pack_start(hbox, False, False, 0)
self.play_button = self.get_play_button()
hbox.pack_start(self.play_button, False, False, 0)
if self.game.is_installed:
hbox.pack_start(self.get_runner_button(), False, False, 0)
hbox.pack_start(self.get_platform_label(), False, False, 0)
if self.game.lastplayed:
hbox.pack_start(self.get_last_played_label(), False, False, 0)
if self.game.playtime:
hbox.pack_start(self.get_playtime_label(), False, False, 0)
hbox.show_all()
def get_popover(self, buttons, parent):
"""Return the popover widget containing a list of link buttons"""
if not buttons:
return None
popover = Gtk.Popover()
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
for action in buttons:
vbox.pack_end(buttons[action], False, False, 1)
popover.add(vbox)
popover.set_position(Gtk.PositionType.TOP)
popover.set_constrain_to(Gtk.PopoverConstraint.NONE)
popover.set_relative_to(parent)
return popover
def get_game_name_label(self):
"""Return the label with the game's title"""
title_label = Gtk.Label(visible=True)
title_label.set_ellipsize(Pango.EllipsizeMode.END)
title_label.set_markup("<span font_desc='16'><b>%s</b></span>" % gtk_safe(self.game.name))
return title_label
def get_runner_button(self):
icon_name = self.game.runner.name + "-symbolic"
runner_icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
runner_icon.show()
box = Gtk.HBox(visible=True)
runner_button = Gtk.Button(visible=True)
popover = self.get_popover(self.get_runner_buttons(), runner_button)
if popover:
runner_button.set_image(runner_icon)
popover_button = Gtk.MenuButton(visible=True)
popover_button.set_size_request(32, 32)
popover_button.props.direction = Gtk.ArrowType.UP
popover_button.set_popover(popover)
runner_button.connect("clicked", lambda _x: popover_button.emit("clicked"))
box.add(runner_button)
box.add(popover_button)
style_context = box.get_style_context()
style_context.add_class("linked")
else:
runner_icon.set_margin_left(49)
runner_icon.set_margin_right(6)
box.add(runner_icon)
return box
def get_platform_label(self):
platform_label = Gtk.Label(visible=True)
platform_label.set_size_request(120, -1)
platform_label.set_alignment(0, 0.5)
platform = gtk_safe(self.game.platform)
platform_label.set_tooltip_markup(platform)
platform_label.set_markup(_("Platform:\n<b>%s</b>") % platform)
platform_label.set_property("ellipsize", Pango.EllipsizeMode.END)
return platform_label
def get_playtime_label(self):
"""Return the label containing the playtime info"""
playtime_label = Gtk.Label(visible=True)
playtime_label.set_size_request(120, -1)
playtime_label.set_alignment(0, 0.5)
playtime_label.set_markup(_("Time played:\n<b>%s</b>") % self.game.formatted_playtime)
return playtime_label
def get_last_played_label(self):
"""Return the label containing the last played info"""
last_played_label = Gtk.Label(visible=True)
last_played_label.set_size_request(120, -1)
last_played_label.set_alignment(0, 0.5)
lastplayed = datetime.fromtimestamp(self.game.lastplayed)
last_played_label.set_markup(_("Last played:\n<b>%s</b>") % lastplayed.strftime("%b %-d %Y"))
return last_played_label
def get_popover_button(self):
"""Return the popover button+menu for the Play button"""
popover_button = Gtk.MenuButton(visible=True)
popover_button.set_size_request(32, 32)
popover_button.props.direction = Gtk.ArrowType.UP
return popover_button
def get_popover_box(self):
"""Return a container for a button + a popover button attached to it"""
box = Gtk.HBox(visible=True)
style_context = box.get_style_context()
style_context.add_class("linked")
return box
def get_locate_installed_game_button(self):
"""Return a button to locate an existing install"""
button = get_link_button("Locate installed game")
button.show()
button.connect("clicked", self.game_actions.on_locate_installed_game, self.game)
return {"locate": button}
def get_play_button(self):
"""Return the widget for install/play/stop and game config"""
button = Gtk.Button(visible=True)
button.set_size_request(120, 32)
box = self.get_popover_box()
popover_button = self.get_popover_button()
if self.game.is_installed:
if self.game.state == self.game.STATE_STOPPED:
button.set_label(_("Play"))
button.connect("clicked", self.game_actions.on_game_launch)
elif self.game.state == self.game.STATE_LAUNCHING:
button.set_label(_("Launching"))
button.set_sensitive(False)
else:
button.set_label(_("Stop"))
button.connect("clicked", self.game_actions.on_game_stop)
else:
button.set_label(_("Install"))
button.connect("clicked", self.game_actions.on_install_clicked)
if self.service:
if self.service.local:
# Local services don't show an install dialog, they can be launched directly
button.set_label(_("Play"))
if self.service.drm_free:
button.set_size_request(84, 32)
box.add(button)
popover = self.get_popover(self.get_locate_installed_game_button(), popover_button)
popover_button.set_popover(popover)
box.add(popover_button)
return box
return button
button.set_size_request(84, 32)
box.add(button)
popover = self.get_popover(self.get_game_buttons(), popover_button)
popover_button.set_popover(popover)
box.add(popover_button)
return box
def get_game_buttons(self):
"""Return a dictionary of buttons to use in the panel"""
displayed = self.game_actions.get_displayed_entries()
buttons = {}
for action in self.game_actions.get_game_actions():
action_id, label, callback = action
if action_id in ("play", "stop", "install"):
continue
button = get_link_button(label)
if displayed.get(action_id):
button.show()
else:
button.hide()
buttons[action_id] = button
button.connect("clicked", self.on_link_button_clicked, callback)
return buttons
def get_runner_buttons(self):
buttons = {}
if self.game.runner_name and self.game.is_installed:
runner = runners.import_runner(self.game.runner_name)(self.game.config)
for entry in runner.context_menu_entries:
name, label, callback = entry
button = get_link_button(label)
button.show()
button.connect("clicked", self.on_link_button_clicked, callback)
buttons[name] = button
return buttons
def on_link_button_clicked(self, button, callback):
"""Callback for link buttons. Closes the popover then runs the actual action"""
popover = button.get_parent().get_parent()
popover.popdown()
callback(button)
def on_install_clicked(self, button):
"""Handler for installing service games"""
self.service.install(self.db_game)
def on_game_state_changed(self, game):
"""Handler called when the game has changed state"""
if (
game.id == self.game.id
or (self.appid and game.appid == self.appid)
):
self.game = game
else:
return True
self.clear_view()
self.update_view()
return True
__init__(self, db_game, game_actions, application)
special
¶
Create the game bar with a database row
Source code in lutris/gui/widgets/game_bar.py
def __init__(self, db_game, game_actions, application):
"""Create the game bar with a database row"""
super().__init__(orientation=Gtk.Orientation.VERTICAL, visible=True,
margin_top=12,
margin_left=12,
margin_bottom=12,
margin_right=12,
spacing=6)
self.game_start_hook_id = GObject.add_emission_hook(Game, "game-start", self.on_game_state_changed)
self.game_started_hook_id = GObject.add_emission_hook(Game, "game-started", self.on_game_state_changed)
self.game_stopped_hook_id = GObject.add_emission_hook(Game, "game-stopped", self.on_game_state_changed)
self.game_updated_hook_id = GObject.add_emission_hook(Game, "game-updated", self.on_game_state_changed)
self.game_removed_hook_id = GObject.add_emission_hook(Game, "game-removed", self.on_game_state_changed)
self.game_installed_hook_id = GObject.add_emission_hook(Game, "game-installed", self.on_game_state_changed)
self.connect("destroy", self.on_destroy)
self.set_margin_bottom(12)
self.game_actions = game_actions
self.db_game = db_game
self.service = None
if db_game.get("service"):
try:
self.service = services.SERVICES[db_game["service"]]()
except KeyError:
pass
game_id = None
if "service_id" in db_game:
self.appid = db_game["service_id"]
game_id = db_game["id"]
elif self.service:
self.appid = db_game["appid"]
if self.service.id == "lutris":
game = get_game_by_field(self.appid, field="slug")
else:
game = get_game_for_service(self.service.id, self.appid)
if game:
game_id = game["id"]
if game_id:
self.game = application.get_game_by_id(game_id) or Game(game_id)
else:
self.game = Game()
self.game.name = db_game["name"]
self.game.slug = db_game["slug"]
self.game.appid = self.appid
self.game.service = self.service.id if self.service else None
game_actions.set_game(self.game)
self.update_view()
clear_view(self)
¶
Clears all widgets from the container
Source code in lutris/gui/widgets/game_bar.py
def clear_view(self):
"""Clears all widgets from the container"""
for child in self.get_children():
child.destroy()
get_game_buttons(self)
¶
Return a dictionary of buttons to use in the panel
Source code in lutris/gui/widgets/game_bar.py
def get_game_buttons(self):
"""Return a dictionary of buttons to use in the panel"""
displayed = self.game_actions.get_displayed_entries()
buttons = {}
for action in self.game_actions.get_game_actions():
action_id, label, callback = action
if action_id in ("play", "stop", "install"):
continue
button = get_link_button(label)
if displayed.get(action_id):
button.show()
else:
button.hide()
buttons[action_id] = button
button.connect("clicked", self.on_link_button_clicked, callback)
return buttons
get_game_name_label(self)
¶
Return the label with the game's title
Source code in lutris/gui/widgets/game_bar.py
def get_game_name_label(self):
"""Return the label with the game's title"""
title_label = Gtk.Label(visible=True)
title_label.set_ellipsize(Pango.EllipsizeMode.END)
title_label.set_markup("<span font_desc='16'><b>%s</b></span>" % gtk_safe(self.game.name))
return title_label
get_last_played_label(self)
¶
Return the label containing the last played info
Source code in lutris/gui/widgets/game_bar.py
def get_last_played_label(self):
"""Return the label containing the last played info"""
last_played_label = Gtk.Label(visible=True)
last_played_label.set_size_request(120, -1)
last_played_label.set_alignment(0, 0.5)
lastplayed = datetime.fromtimestamp(self.game.lastplayed)
last_played_label.set_markup(_("Last played:\n<b>%s</b>") % lastplayed.strftime("%b %-d %Y"))
return last_played_label
get_locate_installed_game_button(self)
¶
Return a button to locate an existing install
Source code in lutris/gui/widgets/game_bar.py
def get_locate_installed_game_button(self):
"""Return a button to locate an existing install"""
button = get_link_button("Locate installed game")
button.show()
button.connect("clicked", self.game_actions.on_locate_installed_game, self.game)
return {"locate": button}
get_platform_label(self)
¶
Source code in lutris/gui/widgets/game_bar.py
def get_platform_label(self):
platform_label = Gtk.Label(visible=True)
platform_label.set_size_request(120, -1)
platform_label.set_alignment(0, 0.5)
platform = gtk_safe(self.game.platform)
platform_label.set_tooltip_markup(platform)
platform_label.set_markup(_("Platform:\n<b>%s</b>") % platform)
platform_label.set_property("ellipsize", Pango.EllipsizeMode.END)
return platform_label
get_play_button(self)
¶
Return the widget for install/play/stop and game config
Source code in lutris/gui/widgets/game_bar.py
def get_play_button(self):
"""Return the widget for install/play/stop and game config"""
button = Gtk.Button(visible=True)
button.set_size_request(120, 32)
box = self.get_popover_box()
popover_button = self.get_popover_button()
if self.game.is_installed:
if self.game.state == self.game.STATE_STOPPED:
button.set_label(_("Play"))
button.connect("clicked", self.game_actions.on_game_launch)
elif self.game.state == self.game.STATE_LAUNCHING:
button.set_label(_("Launching"))
button.set_sensitive(False)
else:
button.set_label(_("Stop"))
button.connect("clicked", self.game_actions.on_game_stop)
else:
button.set_label(_("Install"))
button.connect("clicked", self.game_actions.on_install_clicked)
if self.service:
if self.service.local:
# Local services don't show an install dialog, they can be launched directly
button.set_label(_("Play"))
if self.service.drm_free:
button.set_size_request(84, 32)
box.add(button)
popover = self.get_popover(self.get_locate_installed_game_button(), popover_button)
popover_button.set_popover(popover)
box.add(popover_button)
return box
return button
button.set_size_request(84, 32)
box.add(button)
popover = self.get_popover(self.get_game_buttons(), popover_button)
popover_button.set_popover(popover)
box.add(popover_button)
return box
get_playtime_label(self)
¶
Return the label containing the playtime info
Source code in lutris/gui/widgets/game_bar.py
def get_playtime_label(self):
"""Return the label containing the playtime info"""
playtime_label = Gtk.Label(visible=True)
playtime_label.set_size_request(120, -1)
playtime_label.set_alignment(0, 0.5)
playtime_label.set_markup(_("Time played:\n<b>%s</b>") % self.game.formatted_playtime)
return playtime_label
get_popover(self, buttons, parent)
¶
Return the popover widget containing a list of link buttons
Source code in lutris/gui/widgets/game_bar.py
def get_popover(self, buttons, parent):
"""Return the popover widget containing a list of link buttons"""
if not buttons:
return None
popover = Gtk.Popover()
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
for action in buttons:
vbox.pack_end(buttons[action], False, False, 1)
popover.add(vbox)
popover.set_position(Gtk.PositionType.TOP)
popover.set_constrain_to(Gtk.PopoverConstraint.NONE)
popover.set_relative_to(parent)
return popover
get_popover_box(self)
¶
Return a container for a button + a popover button attached to it
Source code in lutris/gui/widgets/game_bar.py
def get_popover_box(self):
"""Return a container for a button + a popover button attached to it"""
box = Gtk.HBox(visible=True)
style_context = box.get_style_context()
style_context.add_class("linked")
return box
get_popover_button(self)
¶
Return the popover button+menu for the Play button
Source code in lutris/gui/widgets/game_bar.py
def get_popover_button(self):
"""Return the popover button+menu for the Play button"""
popover_button = Gtk.MenuButton(visible=True)
popover_button.set_size_request(32, 32)
popover_button.props.direction = Gtk.ArrowType.UP
return popover_button
get_runner_button(self)
¶
Source code in lutris/gui/widgets/game_bar.py
def get_runner_button(self):
icon_name = self.game.runner.name + "-symbolic"
runner_icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
runner_icon.show()
box = Gtk.HBox(visible=True)
runner_button = Gtk.Button(visible=True)
popover = self.get_popover(self.get_runner_buttons(), runner_button)
if popover:
runner_button.set_image(runner_icon)
popover_button = Gtk.MenuButton(visible=True)
popover_button.set_size_request(32, 32)
popover_button.props.direction = Gtk.ArrowType.UP
popover_button.set_popover(popover)
runner_button.connect("clicked", lambda _x: popover_button.emit("clicked"))
box.add(runner_button)
box.add(popover_button)
style_context = box.get_style_context()
style_context.add_class("linked")
else:
runner_icon.set_margin_left(49)
runner_icon.set_margin_right(6)
box.add(runner_icon)
return box
get_runner_buttons(self)
¶
Source code in lutris/gui/widgets/game_bar.py
def get_runner_buttons(self):
buttons = {}
if self.game.runner_name and self.game.is_installed:
runner = runners.import_runner(self.game.runner_name)(self.game.config)
for entry in runner.context_menu_entries:
name, label, callback = entry
button = get_link_button(label)
button.show()
button.connect("clicked", self.on_link_button_clicked, callback)
buttons[name] = button
return buttons
on_destroy(self, widget)
¶
Source code in lutris/gui/widgets/game_bar.py
def on_destroy(self, widget):
GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)
GObject.remove_emission_hook(Game, "game-started", self.game_started_hook_id)
GObject.remove_emission_hook(Game, "game-stopped", self.game_stopped_hook_id)
GObject.remove_emission_hook(Game, "game-updated", self.game_updated_hook_id)
GObject.remove_emission_hook(Game, "game-removed", self.game_removed_hook_id)
GObject.remove_emission_hook(Game, "game-installed", self.game_installed_hook_id)
return True
on_game_state_changed(self, game)
¶
Handler called when the game has changed state
Source code in lutris/gui/widgets/game_bar.py
def on_game_state_changed(self, game):
"""Handler called when the game has changed state"""
if (
game.id == self.game.id
or (self.appid and game.appid == self.appid)
):
self.game = game
else:
return True
self.clear_view()
self.update_view()
return True
on_install_clicked(self, button)
¶
Handler for installing service games
Source code in lutris/gui/widgets/game_bar.py
def on_install_clicked(self, button):
"""Handler for installing service games"""
self.service.install(self.db_game)
on_link_button_clicked(self, button, callback)
¶
Callback for link buttons. Closes the popover then runs the actual action
Source code in lutris/gui/widgets/game_bar.py
def on_link_button_clicked(self, button, callback):
"""Callback for link buttons. Closes the popover then runs the actual action"""
popover = button.get_parent().get_parent()
popover.popdown()
callback(button)
update_view(self)
¶
Populate the view with widgets
Source code in lutris/gui/widgets/game_bar.py
def update_view(self):
"""Populate the view with widgets"""
game_label = self.get_game_name_label()
game_label.set_halign(Gtk.Align.START)
self.pack_start(game_label, False, False, 0)
hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, spacing=6)
self.pack_start(hbox, False, False, 0)
self.play_button = self.get_play_button()
hbox.pack_start(self.play_button, False, False, 0)
if self.game.is_installed:
hbox.pack_start(self.get_runner_button(), False, False, 0)
hbox.pack_start(self.get_platform_label(), False, False, 0)
if self.game.lastplayed:
hbox.pack_start(self.get_last_played_label(), False, False, 0)
if self.game.playtime:
hbox.pack_start(self.get_playtime_label(), False, False, 0)
hbox.show_all()
gi_composites
¶
GtkTemplate implementation for PyGI
Blog post http://www.virtualroadside.com/blog/index.php/2015/05/24/gtk3-composite-widget-templates-for-python/
Github https://github.com/virtuald/pygi-composite-templates/blob/master/gi_composites.py
This should have landed in PyGObect and will be available without this shim in the future. See: https://gitlab.gnome.org/GNOME/pygobject/merge_requests/52
__all__
special
¶
GtkTemplate
¶
Use this class decorator to signify that a class is a composite widget which will receive widgets and connect to signals as defined in a UI template. You must call init_template to cause the widgets/signals to be initialized from the template::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
def __init__(self):
super().__init__()
self.init_template()
The 'ui' parameter can either be a file path or a GResource resource path::
@GtkTemplate(ui='/org/example/foo.ui')
class Foo(Gtk.Box):
pass
To connect a signal to a method on your instance, do::
@GtkTemplate.Callback
def on_thing_happened(self, widget):
pass
To create a child attribute that is retrieved from your template, add this to your class definition::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
widget = GtkTemplate.Child()
Note: This is implemented as a class decorator, but if it were included with PyGI I suspect it might be better to do this in the GObject metaclass (or similar) so that init_template can be called automatically instead of forcing the user to do it.
.. note:: Due to limitations in PyGObject, you may not inherit from python objects that use the GtkTemplate decorator.
Source code in lutris/gui/widgets/gi_composites.py
class _GtkTemplate:
"""
Use this class decorator to signify that a class is a composite
widget which will receive widgets and connect to signals as
defined in a UI template. You must call init_template to
cause the widgets/signals to be initialized from the template::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
def __init__(self):
super().__init__()
self.init_template()
The 'ui' parameter can either be a file path or a GResource resource
path::
@GtkTemplate(ui='/org/example/foo.ui')
class Foo(Gtk.Box):
pass
To connect a signal to a method on your instance, do::
@GtkTemplate.Callback
def on_thing_happened(self, widget):
pass
To create a child attribute that is retrieved from your template,
add this to your class definition::
@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):
widget = GtkTemplate.Child()
Note: This is implemented as a class decorator, but if it were
included with PyGI I suspect it might be better to do this
in the GObject metaclass (or similar) so that init_template
can be called automatically instead of forcing the user to do it.
.. note:: Due to limitations in PyGObject, you may not inherit from
python objects that use the GtkTemplate decorator.
"""
__ui_path__ = None
@staticmethod
def Callback(f):
"""
Decorator that designates a method to be attached to a signal from
the template
"""
f._gtk_callback = True # pylint: disable=protected-access
return f
Child = _Child
@staticmethod
def set_ui_path(*path):
"""
If using file paths instead of resources, call this *before*
loading anything that uses GtkTemplate, or it will fail to load
your template file
:param path: one or more path elements, will be joined together
to create the final path
TODO: Alternatively, could wait until first class instantiation
before registering templates? Would need a metaclass...
"""
_GtkTemplate.__ui_path__ = abspath(join(*path)) # pylint: disable=no-value-for-parameter
def __init__(self, ui):
self.ui = ui
def __call__(self, cls):
if not issubclass(cls, Gtk.Widget):
raise TypeError("Can only use @GtkTemplate on Widgets")
# Nested templates don't work
if hasattr(cls, "__gtemplate_methods__"):
raise TypeError("Cannot nest template classes")
# Load the template either from a resource path or a file
# - Prefer the resource path first
try:
template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)
except GLib.GError:
ui = self.ui
if isinstance(ui, (list, tuple)):
ui = join(ui)
if _GtkTemplate.__ui_path__ is not None:
ui = join(_GtkTemplate.__ui_path__, ui)
with open(ui, "rb") as fp:
template_bytes = GLib.Bytes.new(fp.read())
_register_template(cls, template_bytes)
return cls
__ui_path__
special
¶
Child
¶
Assign this to an attribute in your class definition and it will be replaced with a widget defined in the UI file when init_template is called
Source code in lutris/gui/widgets/gi_composites.py
class _Child:
"""
Assign this to an attribute in your class definition and it will
be replaced with a widget defined in the UI file when init_template
is called
"""
__slots__ = []
@staticmethod
def widgets(count):
"""
Allows declaring multiple widgets with less typing::
button \
label1 \
label2 = GtkTemplate.Child.widgets(3)
"""
return [_Child() for _ in range(count)]
__slots__
special
¶widgets(count)
staticmethod
¶Allows declaring multiple widgets with less typing::
button label1 label2 = GtkTemplate.Child.widgets(3)
Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def widgets(count):
"""
Allows declaring multiple widgets with less typing::
button \
label1 \
label2 = GtkTemplate.Child.widgets(3)
"""
return [_Child() for _ in range(count)]
Callback(f)
staticmethod
¶
Decorator that designates a method to be attached to a signal from the template
Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def Callback(f):
"""
Decorator that designates a method to be attached to a signal from
the template
"""
f._gtk_callback = True # pylint: disable=protected-access
return f
__call__(self, cls)
special
¶
Source code in lutris/gui/widgets/gi_composites.py
def __call__(self, cls):
if not issubclass(cls, Gtk.Widget):
raise TypeError("Can only use @GtkTemplate on Widgets")
# Nested templates don't work
if hasattr(cls, "__gtemplate_methods__"):
raise TypeError("Cannot nest template classes")
# Load the template either from a resource path or a file
# - Prefer the resource path first
try:
template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)
except GLib.GError:
ui = self.ui
if isinstance(ui, (list, tuple)):
ui = join(ui)
if _GtkTemplate.__ui_path__ is not None:
ui = join(_GtkTemplate.__ui_path__, ui)
with open(ui, "rb") as fp:
template_bytes = GLib.Bytes.new(fp.read())
_register_template(cls, template_bytes)
return cls
__init__(self, ui)
special
¶
Source code in lutris/gui/widgets/gi_composites.py
def __init__(self, ui):
self.ui = ui
set_ui_path(*path)
staticmethod
¶
If using file paths instead of resources, call this before loading anything that uses GtkTemplate, or it will fail to load your template file
:param path: one or more path elements, will be joined together to create the final path
Alternatively, could wait until first class instantiation
before registering templates? Would need a metaclass...
Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def set_ui_path(*path):
"""
If using file paths instead of resources, call this *before*
loading anything that uses GtkTemplate, or it will fail to load
your template file
:param path: one or more path elements, will be joined together
to create the final path
TODO: Alternatively, could wait until first class instantiation
before registering templates? Would need a metaclass...
"""
_GtkTemplate.__ui_path__ = abspath(join(*path)) # pylint: disable=no-value-for-parameter
GtkTemplateWarning (UserWarning)
¶
Source code in lutris/gui/widgets/gi_composites.py
class GtkTemplateWarning(UserWarning):
pass
log_text_view
¶
LogTextView (TextView)
¶
Source code in lutris/gui/widgets/log_text_view.py
class LogTextView(Gtk.TextView):
# pylint: disable=no-member
def __init__(self, buffer=None, autoscroll=True):
super().__init__(visible=True)
if buffer:
self.set_buffer(buffer)
self.set_editable(False)
self.set_cursor_visible(False)
self.set_monospace(True)
self.set_left_margin(10)
self.scroll_max = 0
self.set_wrap_mode(Gtk.WrapMode.CHAR)
self.get_style_context().add_class("lutris-logview")
self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
if autoscroll:
self.connect("size-allocate", self.autoscroll)
def autoscroll(self, *args): # pylint: disable=unused-argument
adj = self.get_vadjustment()
if adj.get_value() == self.scroll_max or self.scroll_max == 0:
adj.set_value(adj.get_upper() - adj.get_page_size())
self.scroll_max = adj.get_value()
else:
self.scroll_max = adj.get_upper() - adj.get_page_size()
def create_new_mark(self, buffer_iter):
return self.props.buffer.create_mark(None, buffer_iter, True)
def reset_search(self):
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
self.props.buffer.place_cursor(self.props.buffer.get_iter_at_mark(self.mark))
def find_first(self, searched_entry):
self.reset_search()
self.find_next(searched_entry)
def find_next(self, searched_entry):
buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
next_occurence = buffer_iter.forward_search(
searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
)
# Found nothing try from the beginning
if next_occurence is None:
next_occurence = self.props.buffer.get_start_iter(
).forward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)
# Highlight if result
if next_occurence is not None:
self.highlight(next_occurence[0], next_occurence[1])
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(next_occurence[1])
def find_previous(self, searched_entry):
# First go to the beginning of searched_entry string
buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
buffer_iter.backward_chars(len(searched_entry.get_text()))
previous_occurence = buffer_iter.backward_search(
searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
)
# Found nothing ? Try from the end
if previous_occurence is None:
previous_occurence = self.props.buffer.get_end_iter(
).backward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)
# Highlight if result
if previous_occurence is not None:
self.highlight(previous_occurence[0], previous_occurence[1])
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(previous_occurence[1])
def highlight(self, range_start, range_end):
self.props.buffer.select_range(range_start, range_end)
# Focus
self.scroll_mark_onscreen(self.mark)
__init__(self, buffer=None, autoscroll=True)
special
¶
Source code in lutris/gui/widgets/log_text_view.py
def __init__(self, buffer=None, autoscroll=True):
super().__init__(visible=True)
if buffer:
self.set_buffer(buffer)
self.set_editable(False)
self.set_cursor_visible(False)
self.set_monospace(True)
self.set_left_margin(10)
self.scroll_max = 0
self.set_wrap_mode(Gtk.WrapMode.CHAR)
self.get_style_context().add_class("lutris-logview")
self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
if autoscroll:
self.connect("size-allocate", self.autoscroll)
autoscroll(self, *args)
¶
Source code in lutris/gui/widgets/log_text_view.py
def autoscroll(self, *args): # pylint: disable=unused-argument
adj = self.get_vadjustment()
if adj.get_value() == self.scroll_max or self.scroll_max == 0:
adj.set_value(adj.get_upper() - adj.get_page_size())
self.scroll_max = adj.get_value()
else:
self.scroll_max = adj.get_upper() - adj.get_page_size()
create_new_mark(self, buffer_iter)
¶
Source code in lutris/gui/widgets/log_text_view.py
def create_new_mark(self, buffer_iter):
return self.props.buffer.create_mark(None, buffer_iter, True)
find_first(self, searched_entry)
¶
Source code in lutris/gui/widgets/log_text_view.py
def find_first(self, searched_entry):
self.reset_search()
self.find_next(searched_entry)
find_next(self, searched_entry)
¶
Source code in lutris/gui/widgets/log_text_view.py
def find_next(self, searched_entry):
buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
next_occurence = buffer_iter.forward_search(
searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
)
# Found nothing try from the beginning
if next_occurence is None:
next_occurence = self.props.buffer.get_start_iter(
).forward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)
# Highlight if result
if next_occurence is not None:
self.highlight(next_occurence[0], next_occurence[1])
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(next_occurence[1])
find_previous(self, searched_entry)
¶
Source code in lutris/gui/widgets/log_text_view.py
def find_previous(self, searched_entry):
# First go to the beginning of searched_entry string
buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
buffer_iter.backward_chars(len(searched_entry.get_text()))
previous_occurence = buffer_iter.backward_search(
searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
)
# Found nothing ? Try from the end
if previous_occurence is None:
previous_occurence = self.props.buffer.get_end_iter(
).backward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)
# Highlight if result
if previous_occurence is not None:
self.highlight(previous_occurence[0], previous_occurence[1])
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(previous_occurence[1])
highlight(self, range_start, range_end)
¶
Source code in lutris/gui/widgets/log_text_view.py
def highlight(self, range_start, range_end):
self.props.buffer.select_range(range_start, range_end)
# Focus
self.scroll_mark_onscreen(self.mark)
reset_search(self)
¶
Source code in lutris/gui/widgets/log_text_view.py
def reset_search(self):
self.props.buffer.delete_mark(self.mark)
self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
self.props.buffer.place_cursor(self.props.buffer.get_iter_at_mark(self.mark))
notifications
¶
send_notification(title, text, file_path_to_icon='lutris')
¶
Source code in lutris/gui/widgets/notifications.py
def send_notification(title, text, file_path_to_icon="lutris"):
icon_file = Gio.File.new_for_path(file_path_to_icon)
icon = Gio.FileIcon.new(icon_file)
notification = Gio.Notification.new(title)
notification.set_body(text)
notification.set_icon(icon)
application = Gio.Application.get_default()
application.send_notification(None, notification)
logger.info(title)
logger.info(text)
searchable_combobox
¶
Extended combobox with search
SearchableCombobox (Bin)
¶
Combox box with autocompletion. Well fitted for large lists.
Source code in lutris/gui/widgets/searchable_combobox.py
class SearchableCombobox(Gtk.Bin):
"""Combox box with autocompletion.
Well fitted for large lists.
"""
__gsignals__ = {
"changed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
}
def __init__(self, choice_func, initial=None):
super().__init__()
self.initial = initial
self.liststore = Gtk.ListStore(str, str)
self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.liststore)
self.combobox.set_entry_text_column(0)
self.combobox.set_id_column(1)
self.combobox.set_valign(Gtk.Align.CENTER)
completion = Gtk.EntryCompletion()
completion.set_model(self.liststore)
completion.set_text_column(0)
completion.set_match_func(self.search_store)
completion.connect("match-selected", self.set_id_from_completion)
entry = self.combobox.get_child()
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "content-loading-symbolic")
entry.set_completion(completion)
self.combobox.connect("changed", self.on_combobox_change)
self.combobox.connect("scroll-event", self._on_combobox_scroll)
self.add(self.combobox)
GLib.idle_add(self._populate_combobox_choices, choice_func)
def get_model(self):
"""Proxy to the liststore"""
return self.liststore
def get_active(self):
"""Proxy to the get_active method"""
return self.combobox.get_active()
@staticmethod
def get_has_entry():
"""The entry present is not for editing custom values, only search"""
return False
def search_store(self, _completion, string, _iter):
"""Return true if any word of a string is present in a row"""
for word in string.split():
if word not in self.liststore[_iter][0].lower(): # search is always lower case
return False
return True
def set_id_from_completion(self, _completion, model, _iter):
"""Sets the active ID to the appropriate ID column in the model
otherwise the value is set to the entry's value.
"""
self.combobox.set_active_id(model[_iter][1])
def _populate_combobox_choices(self, choice_func):
AsyncCall(self._do_populate_combobox_choices, None, choice_func)
def _do_populate_combobox_choices(self, choice_func):
for choice in choice_func():
self.liststore.append(choice)
entry = self.combobox.get_child()
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, None)
self.combobox.set_active_id(self.initial)
@staticmethod
def _on_combobox_scroll(combobox, _event):
"""Prevents users from accidentally changing configuration values
while scrolling down dialogs.
"""
combobox.stop_emission_by_name("scroll-event")
return False
def on_combobox_change(self, _widget):
"""Action triggered on combobox 'changed' signal."""
active = self.combobox.get_active()
if active < 0:
return
option_value = self.liststore[active][1]
self.emit("changed", option_value)
__init__(self, choice_func, initial=None)
special
¶
Source code in lutris/gui/widgets/searchable_combobox.py
def __init__(self, choice_func, initial=None):
super().__init__()
self.initial = initial
self.liststore = Gtk.ListStore(str, str)
self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.liststore)
self.combobox.set_entry_text_column(0)
self.combobox.set_id_column(1)
self.combobox.set_valign(Gtk.Align.CENTER)
completion = Gtk.EntryCompletion()
completion.set_model(self.liststore)
completion.set_text_column(0)
completion.set_match_func(self.search_store)
completion.connect("match-selected", self.set_id_from_completion)
entry = self.combobox.get_child()
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "content-loading-symbolic")
entry.set_completion(completion)
self.combobox.connect("changed", self.on_combobox_change)
self.combobox.connect("scroll-event", self._on_combobox_scroll)
self.add(self.combobox)
GLib.idle_add(self._populate_combobox_choices, choice_func)
get_active(self)
¶
Proxy to the get_active method
Source code in lutris/gui/widgets/searchable_combobox.py
def get_active(self):
"""Proxy to the get_active method"""
return self.combobox.get_active()
get_has_entry()
staticmethod
¶
The entry present is not for editing custom values, only search
Source code in lutris/gui/widgets/searchable_combobox.py
@staticmethod
def get_has_entry():
"""The entry present is not for editing custom values, only search"""
return False
get_model(self)
¶
Proxy to the liststore
Source code in lutris/gui/widgets/searchable_combobox.py
def get_model(self):
"""Proxy to the liststore"""
return self.liststore
on_combobox_change(self, _widget)
¶
Action triggered on combobox 'changed' signal.
Source code in lutris/gui/widgets/searchable_combobox.py
def on_combobox_change(self, _widget):
"""Action triggered on combobox 'changed' signal."""
active = self.combobox.get_active()
if active < 0:
return
option_value = self.liststore[active][1]
self.emit("changed", option_value)
search_store(self, _completion, string, _iter)
¶
Return true if any word of a string is present in a row
Source code in lutris/gui/widgets/searchable_combobox.py
def search_store(self, _completion, string, _iter):
"""Return true if any word of a string is present in a row"""
for word in string.split():
if word not in self.liststore[_iter][0].lower(): # search is always lower case
return False
return True
set_id_from_completion(self, _completion, model, _iter)
¶
Sets the active ID to the appropriate ID column in the model otherwise the value is set to the entry's value.
Source code in lutris/gui/widgets/searchable_combobox.py
def set_id_from_completion(self, _completion, model, _iter):
"""Sets the active ID to the appropriate ID column in the model
otherwise the value is set to the entry's value.
"""
self.combobox.set_active_id(model[_iter][1])
sidebar
¶
Sidebar for the main window
GAMECOUNT
¶
ICON
¶
LABEL
¶
SLUG
¶
TYPE
¶
DummyRow
¶
Dummy class for rows that may not be initialized.
Source code in lutris/gui/widgets/sidebar.py
class DummyRow():
"""Dummy class for rows that may not be initialized."""
def show(self):
"""Dummy method for showing the row"""
def hide(self):
"""Dummy method for hiding the row"""
LutrisSidebar (ListBox)
¶
Source code in lutris/gui/widgets/sidebar.py
class LutrisSidebar(Gtk.ListBox):
__gtype_name__ = "LutrisSidebar"
def __init__(self, application, selected=None):
super().__init__()
self.set_size_request(200, -1)
self.application = application
self.get_style_context().add_class("sidebar")
self.installed_runners = []
self.service_rows = {}
self.active_platforms = None
self.runners = None
self.platforms = None
self.categories = None
# A dummy objects that allows inspecting why/when we have a show() call on the object.
self.running_row = DummyRow()
if selected:
self.selected_row_type, self.selected_row_id = selected.split(":")
else:
self.selected_row_type, self.selected_row_id = ("category", "all")
self.row_headers = {
"library": SidebarHeader(_("Library")),
"sources": SidebarHeader(_("Sources")),
"runners": SidebarHeader(_("Runners")),
"platforms": SidebarHeader(_("Platforms")),
}
GObject.add_emission_hook(RunnerBox, "runner-installed", self.update)
GObject.add_emission_hook(RunnerBox, "runner-removed", self.update)
GObject.add_emission_hook(ServicesBox, "services-changed", self.on_services_changed)
GObject.add_emission_hook(Game, "game-start", self.on_game_start)
GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
GObject.add_emission_hook(Game, "game-updated", self.update)
GObject.add_emission_hook(Game, "game-removed", self.update)
GObject.add_emission_hook(BaseService, "service-login", self.on_service_auth_changed)
GObject.add_emission_hook(BaseService, "service-logout", self.on_service_auth_changed)
GObject.add_emission_hook(BaseService, "service-games-load", self.on_service_games_updating)
GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
self.set_filter_func(self._filter_func)
self.set_header_func(self._header_func)
self.show_all()
def get_sidebar_icon(self, icon_name):
name = icon_name if has_stock_icon(icon_name) else "package-x-generic-symbolic"
icon = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU)
# We can wind up with an icon of the wrong size, if that's what is
# available. So we'll fix that.
icon_size = Gtk.IconSize.lookup(Gtk.IconSize.MENU)
if icon_size[0]:
icon.set_pixel_size(icon_size[2])
return icon
def initialize_rows(self):
"""
Select the initial row; this triggers the initialization of the game view
so we must do this even if this sidebar is never realized, but only after
the sidebar's signals are connected.
"""
self.active_platforms = games_db.get_used_platforms()
self.runners = sorted(runners.__all__)
self.platforms = sorted(runners.RUNNER_PLATFORMS)
self.categories = categories_db.get_categories()
self.add(
SidebarRow(
"all",
"category",
_("Games"),
Gtk.Image.new_from_icon_name("applications-games-symbolic", Gtk.IconSize.MENU)
)
)
self.add(
SidebarRow(
"recent",
"dynamic_category",
_("Recent"),
Gtk.Image.new_from_icon_name("document-open-recent-symbolic", Gtk.IconSize.MENU)
)
)
self.add(
SidebarRow(
"favorite",
"category",
_("Favorites"),
Gtk.Image.new_from_icon_name("favorite-symbolic", Gtk.IconSize.MENU)
)
)
self.running_row = SidebarRow(
"running",
"dynamic_category",
_("Running"),
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.MENU)
)
# I wanted this to be on top but it really messes with the headers when showing/hiding the row.
self.add(self.running_row)
service_classes = services.get_enabled_services()
for service_name in service_classes:
service = service_classes[service_name]()
row_class = OnlineServiceSidebarRow if service.online else ServiceSidebarRow
service_row = row_class(service)
self.service_rows[service_name] = service_row
self.add(service_row)
for runner_name in self.runners:
icon_name = runner_name.lower().replace(" ", "") + "-symbolic"
runner = runners.import_runner(runner_name)()
self.add(RunnerSidebarRow(
runner_name,
"runner",
runner.human_name,
self.get_sidebar_icon(icon_name),
application=self.application
))
for platform in self.platforms:
icon_name = (platform.lower().replace(" ", "").replace("/", "_") + "-symbolic")
self.add(SidebarRow(platform, "platform", platform, self.get_sidebar_icon(icon_name)))
self.update()
for row in self.get_children():
if row.type == self.selected_row_type and row.id == self.selected_row_id:
self.select_row(row)
break
self.show_all()
self.running_row.hide()
def _filter_func(self, row):
if not row or not row.id or row.type in ("category", "dynamic_category", "service"):
return True
if row.type == "runner":
if row.id is None:
return True # 'All'
return row.id in self.installed_runners
return row.id in self.active_platforms
def _header_func(self, row, before):
if not before:
row.set_header(self.row_headers["library"])
elif before.type in ("category", "dynamic_category") and row.type == "service":
row.set_header(self.row_headers["sources"])
elif before.type == "service" and row.type == "runner":
row.set_header(self.row_headers["runners"])
elif before.type == "runner" and row.type == "platform":
row.set_header(self.row_headers["platforms"])
else:
row.set_header(None)
def update(self, *_args):
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.active_platforms = games_db.get_used_platforms()
self.invalidate_filter()
return True
def on_game_start(self, _game):
"""Show the "running" section when a game start"""
self.running_row.show()
return True
def on_game_stop(self, _game):
"""Hide the "running" section when no games are running"""
if not self.application.running_games.get_n_items():
self.running_row.hide()
if self.get_selected_row() == self.running_row:
self.select_row(self.get_children()[0])
return True
def on_service_auth_changed(self, service):
self.service_rows[service.id].create_button_box()
self.service_rows[service.id].update_buttons()
return True
def on_service_games_updating(self, service):
self.service_rows[service.id].is_updating = True
self.service_rows[service.id].update_buttons()
return True
def on_service_games_updated(self, service):
self.service_rows[service.id].is_updating = False
self.service_rows[service.id].update_buttons()
return True
def on_services_changed(self, _widget):
for child in self.get_children():
child.destroy()
self.initialize_rows()
return True
__gtype_name__
special
¶
__init__(self, application, selected=None)
special
¶
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, application, selected=None):
super().__init__()
self.set_size_request(200, -1)
self.application = application
self.get_style_context().add_class("sidebar")
self.installed_runners = []
self.service_rows = {}
self.active_platforms = None
self.runners = None
self.platforms = None
self.categories = None
# A dummy objects that allows inspecting why/when we have a show() call on the object.
self.running_row = DummyRow()
if selected:
self.selected_row_type, self.selected_row_id = selected.split(":")
else:
self.selected_row_type, self.selected_row_id = ("category", "all")
self.row_headers = {
"library": SidebarHeader(_("Library")),
"sources": SidebarHeader(_("Sources")),
"runners": SidebarHeader(_("Runners")),
"platforms": SidebarHeader(_("Platforms")),
}
GObject.add_emission_hook(RunnerBox, "runner-installed", self.update)
GObject.add_emission_hook(RunnerBox, "runner-removed", self.update)
GObject.add_emission_hook(ServicesBox, "services-changed", self.on_services_changed)
GObject.add_emission_hook(Game, "game-start", self.on_game_start)
GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
GObject.add_emission_hook(Game, "game-updated", self.update)
GObject.add_emission_hook(Game, "game-removed", self.update)
GObject.add_emission_hook(BaseService, "service-login", self.on_service_auth_changed)
GObject.add_emission_hook(BaseService, "service-logout", self.on_service_auth_changed)
GObject.add_emission_hook(BaseService, "service-games-load", self.on_service_games_updating)
GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
self.set_filter_func(self._filter_func)
self.set_header_func(self._header_func)
self.show_all()
get_sidebar_icon(self, icon_name)
¶
Source code in lutris/gui/widgets/sidebar.py
def get_sidebar_icon(self, icon_name):
name = icon_name if has_stock_icon(icon_name) else "package-x-generic-symbolic"
icon = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU)
# We can wind up with an icon of the wrong size, if that's what is
# available. So we'll fix that.
icon_size = Gtk.IconSize.lookup(Gtk.IconSize.MENU)
if icon_size[0]:
icon.set_pixel_size(icon_size[2])
return icon
initialize_rows(self)
¶
Select the initial row; this triggers the initialization of the game view so we must do this even if this sidebar is never realized, but only after the sidebar's signals are connected.
Source code in lutris/gui/widgets/sidebar.py
def initialize_rows(self):
"""
Select the initial row; this triggers the initialization of the game view
so we must do this even if this sidebar is never realized, but only after
the sidebar's signals are connected.
"""
self.active_platforms = games_db.get_used_platforms()
self.runners = sorted(runners.__all__)
self.platforms = sorted(runners.RUNNER_PLATFORMS)
self.categories = categories_db.get_categories()
self.add(
SidebarRow(
"all",
"category",
_("Games"),
Gtk.Image.new_from_icon_name("applications-games-symbolic", Gtk.IconSize.MENU)
)
)
self.add(
SidebarRow(
"recent",
"dynamic_category",
_("Recent"),
Gtk.Image.new_from_icon_name("document-open-recent-symbolic", Gtk.IconSize.MENU)
)
)
self.add(
SidebarRow(
"favorite",
"category",
_("Favorites"),
Gtk.Image.new_from_icon_name("favorite-symbolic", Gtk.IconSize.MENU)
)
)
self.running_row = SidebarRow(
"running",
"dynamic_category",
_("Running"),
Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.MENU)
)
# I wanted this to be on top but it really messes with the headers when showing/hiding the row.
self.add(self.running_row)
service_classes = services.get_enabled_services()
for service_name in service_classes:
service = service_classes[service_name]()
row_class = OnlineServiceSidebarRow if service.online else ServiceSidebarRow
service_row = row_class(service)
self.service_rows[service_name] = service_row
self.add(service_row)
for runner_name in self.runners:
icon_name = runner_name.lower().replace(" ", "") + "-symbolic"
runner = runners.import_runner(runner_name)()
self.add(RunnerSidebarRow(
runner_name,
"runner",
runner.human_name,
self.get_sidebar_icon(icon_name),
application=self.application
))
for platform in self.platforms:
icon_name = (platform.lower().replace(" ", "").replace("/", "_") + "-symbolic")
self.add(SidebarRow(platform, "platform", platform, self.get_sidebar_icon(icon_name)))
self.update()
for row in self.get_children():
if row.type == self.selected_row_type and row.id == self.selected_row_id:
self.select_row(row)
break
self.show_all()
self.running_row.hide()
on_game_start(self, _game)
¶
Show the "running" section when a game start
Source code in lutris/gui/widgets/sidebar.py
def on_game_start(self, _game):
"""Show the "running" section when a game start"""
self.running_row.show()
return True
on_game_stop(self, _game)
¶
Hide the "running" section when no games are running
Source code in lutris/gui/widgets/sidebar.py
def on_game_stop(self, _game):
"""Hide the "running" section when no games are running"""
if not self.application.running_games.get_n_items():
self.running_row.hide()
if self.get_selected_row() == self.running_row:
self.select_row(self.get_children()[0])
return True
on_service_auth_changed(self, service)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_service_auth_changed(self, service):
self.service_rows[service.id].create_button_box()
self.service_rows[service.id].update_buttons()
return True
on_service_games_updated(self, service)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_service_games_updated(self, service):
self.service_rows[service.id].is_updating = False
self.service_rows[service.id].update_buttons()
return True
on_service_games_updating(self, service)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_service_games_updating(self, service):
self.service_rows[service.id].is_updating = True
self.service_rows[service.id].update_buttons()
return True
on_services_changed(self, _widget)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_services_changed(self, _widget):
for child in self.get_children():
child.destroy()
self.initialize_rows()
return True
update(self, *_args)
¶
Source code in lutris/gui/widgets/sidebar.py
def update(self, *_args):
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.active_platforms = games_db.get_used_platforms()
self.invalidate_filter()
return True
OnlineServiceSidebarRow (ServiceSidebarRow)
¶
Source code in lutris/gui/widgets/sidebar.py
class OnlineServiceSidebarRow(ServiceSidebarRow):
def get_buttons(self):
return {
"run": (("media-playback-start-symbolic", _("Run"), self.on_service_run, "run")),
"refresh": ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh"),
"disconnect": ("system-log-out-symbolic", _("Disconnect"), self.on_connect_clicked, "disconnect"),
"connect": ("avatar-default-symbolic", _("Connect"), self.on_connect_clicked, "connect")
}
def get_actions(self):
buttons = self.get_buttons()
displayed_buttons = []
if self.service.is_launchable():
displayed_buttons.append(buttons["run"])
if self.service.is_authenticated():
displayed_buttons += [buttons["refresh"], buttons["disconnect"]]
else:
displayed_buttons += [buttons["connect"]]
return displayed_buttons
def on_connect_clicked(self, button):
button.set_sensitive(False)
if self.service.is_authenticated():
self.service.logout()
else:
self.service.login()
self.create_button_box()
get_actions(self)
¶
Return the definition of buttons to be added to the row
Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
buttons = self.get_buttons()
displayed_buttons = []
if self.service.is_launchable():
displayed_buttons.append(buttons["run"])
if self.service.is_authenticated():
displayed_buttons += [buttons["refresh"], buttons["disconnect"]]
else:
displayed_buttons += [buttons["connect"]]
return displayed_buttons
get_buttons(self)
¶
Source code in lutris/gui/widgets/sidebar.py
def get_buttons(self):
return {
"run": (("media-playback-start-symbolic", _("Run"), self.on_service_run, "run")),
"refresh": ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh"),
"disconnect": ("system-log-out-symbolic", _("Disconnect"), self.on_connect_clicked, "disconnect"),
"connect": ("avatar-default-symbolic", _("Connect"), self.on_connect_clicked, "connect")
}
on_connect_clicked(self, button)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_connect_clicked(self, button):
button.set_sensitive(False)
if self.service.is_authenticated():
self.service.logout()
else:
self.service.login()
self.create_button_box()
RunnerSidebarRow (SidebarRow)
¶
Source code in lutris/gui/widgets/sidebar.py
class RunnerSidebarRow(SidebarRow):
def get_actions(self):
"""Return the definition of buttons to be added to the row"""
if not self.id:
return []
entries = []
# Creation is delayed because only installed runners can be imported
# and all visible boxes should be installed.
self.runner = runners.import_runner(self.id)()
if self.runner.multiple_versions:
entries.append((
"system-software-install-symbolic",
_("Manage Versions"),
self.on_manage_versions,
"manage-versions"
))
if self.runner.runnable_alone:
entries.append(("media-playback-start-symbolic", _("Run"), self.runner.run, "run"))
entries.append(("emblem-system-symbolic", _("Configure"), self.on_configure_runner, "configure"))
return entries
def on_configure_runner(self, *_args):
"""Show runner configuration"""
self.application.show_window(RunnerConfigDialog, runner=self.runner)
def on_manage_versions(self, *_args):
"""Manage runner versions"""
dlg_title = _("Manage %s versions") % self.runner.name
RunnerInstallDialog(dlg_title, self.get_toplevel(), self.runner.name)
get_actions(self)
¶
Return the definition of buttons to be added to the row
Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
"""Return the definition of buttons to be added to the row"""
if not self.id:
return []
entries = []
# Creation is delayed because only installed runners can be imported
# and all visible boxes should be installed.
self.runner = runners.import_runner(self.id)()
if self.runner.multiple_versions:
entries.append((
"system-software-install-symbolic",
_("Manage Versions"),
self.on_manage_versions,
"manage-versions"
))
if self.runner.runnable_alone:
entries.append(("media-playback-start-symbolic", _("Run"), self.runner.run, "run"))
entries.append(("emblem-system-symbolic", _("Configure"), self.on_configure_runner, "configure"))
return entries
on_configure_runner(self, *_args)
¶
Show runner configuration
Source code in lutris/gui/widgets/sidebar.py
def on_configure_runner(self, *_args):
"""Show runner configuration"""
self.application.show_window(RunnerConfigDialog, runner=self.runner)
on_manage_versions(self, *_args)
¶
Manage runner versions
Source code in lutris/gui/widgets/sidebar.py
def on_manage_versions(self, *_args):
"""Manage runner versions"""
dlg_title = _("Manage %s versions") % self.runner.name
RunnerInstallDialog(dlg_title, self.get_toplevel(), self.runner.name)
ServiceSidebarRow (SidebarRow)
¶
Source code in lutris/gui/widgets/sidebar.py
class ServiceSidebarRow(SidebarRow):
def __init__(self, service):
super().__init__(
service.id,
"service",
service.name,
Gtk.Image.new_from_icon_name(service.icon, Gtk.IconSize.MENU)
)
self.service = service
def get_actions(self):
"""Return the definition of buttons to be added to the row"""
return [
("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh")
]
def on_service_run(self, button):
"""Run a launcher associated with a service"""
self.service.run()
def on_refresh_clicked(self, button):
"""Reload the service games"""
button.set_sensitive(False)
if self.service.online and not self.service.is_connected():
self.service.logout()
return
AsyncCall(self.service.reload, self.service_load_cb)
def service_load_cb(self, _result, error):
if error:
if isinstance(error, AuthTokenExpired):
self.service.logout()
self.service.login()
else:
ErrorDialog(str(error))
GLib.timeout_add(2000, self.enable_refresh_button)
def enable_refresh_button(self):
self.buttons["refresh"].set_sensitive(True)
return False
__init__(self, service)
special
¶
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, service):
super().__init__(
service.id,
"service",
service.name,
Gtk.Image.new_from_icon_name(service.icon, Gtk.IconSize.MENU)
)
self.service = service
enable_refresh_button(self)
¶
Source code in lutris/gui/widgets/sidebar.py
def enable_refresh_button(self):
self.buttons["refresh"].set_sensitive(True)
return False
get_actions(self)
¶
Return the definition of buttons to be added to the row
Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
"""Return the definition of buttons to be added to the row"""
return [
("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh")
]
on_refresh_clicked(self, button)
¶
Reload the service games
Source code in lutris/gui/widgets/sidebar.py
def on_refresh_clicked(self, button):
"""Reload the service games"""
button.set_sensitive(False)
if self.service.online and not self.service.is_connected():
self.service.logout()
return
AsyncCall(self.service.reload, self.service_load_cb)
on_service_run(self, button)
¶
Run a launcher associated with a service
Source code in lutris/gui/widgets/sidebar.py
def on_service_run(self, button):
"""Run a launcher associated with a service"""
self.service.run()
service_load_cb(self, _result, error)
¶
Source code in lutris/gui/widgets/sidebar.py
def service_load_cb(self, _result, error):
if error:
if isinstance(error, AuthTokenExpired):
self.service.logout()
self.service.login()
else:
ErrorDialog(str(error))
GLib.timeout_add(2000, self.enable_refresh_button)
SidebarHeader (Box)
¶
Header shown on top of each sidebar section
Source code in lutris/gui/widgets/sidebar.py
class SidebarHeader(Gtk.Box):
"""Header shown on top of each sidebar section"""
def __init__(self, name):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.get_style_context().add_class("sidebar-header")
label = Gtk.Label(
halign=Gtk.Align.START,
hexpand=True,
use_markup=True,
label="<b>{}</b>".format(name),
)
label.get_style_context().add_class("dim-label")
box = Gtk.Box(margin_start=9, margin_top=6, margin_bottom=6, margin_right=9)
box.add(label)
self.add(box)
self.add(Gtk.Separator())
self.show_all()
__init__(self, name)
special
¶
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, name):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.get_style_context().add_class("sidebar-header")
label = Gtk.Label(
halign=Gtk.Align.START,
hexpand=True,
use_markup=True,
label="<b>{}</b>".format(name),
)
label.get_style_context().add_class("dim-label")
box = Gtk.Box(margin_start=9, margin_top=6, margin_bottom=6, margin_right=9)
box.add(label)
self.add(box)
self.add(Gtk.Separator())
self.show_all()
SidebarRow (ListBoxRow)
¶
A row in the sidebar containing possible action buttons
Source code in lutris/gui/widgets/sidebar.py
class SidebarRow(Gtk.ListBoxRow):
"""A row in the sidebar containing possible action buttons"""
MARGIN = 9
SPACING = 6
def __init__(self, id_, type_, name, icon, application=None):
"""Initialize the row
Parameters:
id_: identifier of the row
type: type of row to display (still used?)
name (str): Text displayed on the row
icon (GtkImage): icon displayed next to the label
application (GtkApplication): reference to the running application
"""
super().__init__()
self.application = application
self.type = type_
self.id = id_
self.runner = None
self.name = name
self.is_updating = False
self.buttons = {}
self.box = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
self.connect("realize", self.on_realize)
self.add(self.box)
if not icon:
icon = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
self.box.add(icon)
label = Gtk.Label(
label=name,
halign=Gtk.Align.START,
hexpand=True,
margin_top=self.SPACING,
margin_bottom=self.SPACING,
ellipsize=Pango.EllipsizeMode.END,
)
self.box.pack_start(label, True, True, 0)
self.btn_box = Gtk.Box(spacing=3, no_show_all=True, valign=Gtk.Align.CENTER, homogeneous=True)
self.box.pack_end(self.btn_box, False, False, 0)
self.spinner = Gtk.Spinner()
self.box.pack_end(self.spinner, False, False, 0)
def get_actions(self):
return []
def is_row_active(self):
"""Return true if the row is hovered or is the one selected"""
flags = self.get_state_flags()
# Naming things sure is hard... But "prelight" instead of "hover"? Come on...
return flags & Gtk.StateFlags.PRELIGHT or flags & Gtk.StateFlags.SELECTED
def do_state_flags_changed(self, previous_flags): # pylint: disable=arguments-differ
if self.id:
self.update_buttons()
Gtk.ListBoxRow.do_state_flags_changed(self, previous_flags)
def update_buttons(self):
if self.is_updating:
self.btn_box.hide()
self.spinner.show()
self.spinner.start()
return
self.spinner.stop()
self.spinner.hide()
if self.is_row_active():
self.btn_box.show()
elif self.btn_box.get_visible():
self.btn_box.hide()
def create_button_box(self):
"""Adds buttons in the button box based on the row's actions"""
for child in self.btn_box.get_children():
child.destroy()
for action in self.get_actions():
btn = Gtk.Button(tooltip_text=action[1], relief=Gtk.ReliefStyle.NONE, visible=True)
image = Gtk.Image.new_from_icon_name(action[0], Gtk.IconSize.MENU)
image.show()
btn.add(image)
btn.connect("clicked", action[2])
self.buttons[action[3]] = btn
self.btn_box.add(btn)
def on_realize(self, widget):
self.create_button_box()
MARGIN
¶
SPACING
¶
__init__(self, id_, type_, name, icon, application=None)
special
¶
Initialize the row
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_ |
identifier of the row |
required | |
type |
type of row to display (still used?) |
required | |
name |
str |
Text displayed on the row |
required |
icon |
GtkImage |
icon displayed next to the label |
required |
application |
GtkApplication |
reference to the running application |
None |
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, id_, type_, name, icon, application=None):
"""Initialize the row
Parameters:
id_: identifier of the row
type: type of row to display (still used?)
name (str): Text displayed on the row
icon (GtkImage): icon displayed next to the label
application (GtkApplication): reference to the running application
"""
super().__init__()
self.application = application
self.type = type_
self.id = id_
self.runner = None
self.name = name
self.is_updating = False
self.buttons = {}
self.box = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
self.connect("realize", self.on_realize)
self.add(self.box)
if not icon:
icon = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
self.box.add(icon)
label = Gtk.Label(
label=name,
halign=Gtk.Align.START,
hexpand=True,
margin_top=self.SPACING,
margin_bottom=self.SPACING,
ellipsize=Pango.EllipsizeMode.END,
)
self.box.pack_start(label, True, True, 0)
self.btn_box = Gtk.Box(spacing=3, no_show_all=True, valign=Gtk.Align.CENTER, homogeneous=True)
self.box.pack_end(self.btn_box, False, False, 0)
self.spinner = Gtk.Spinner()
self.box.pack_end(self.spinner, False, False, 0)
create_button_box(self)
¶
Adds buttons in the button box based on the row's actions
Source code in lutris/gui/widgets/sidebar.py
def create_button_box(self):
"""Adds buttons in the button box based on the row's actions"""
for child in self.btn_box.get_children():
child.destroy()
for action in self.get_actions():
btn = Gtk.Button(tooltip_text=action[1], relief=Gtk.ReliefStyle.NONE, visible=True)
image = Gtk.Image.new_from_icon_name(action[0], Gtk.IconSize.MENU)
image.show()
btn.add(image)
btn.connect("clicked", action[2])
self.buttons[action[3]] = btn
self.btn_box.add(btn)
do_state_flags_changed(self, previous_flags)
¶
state_flags_changed(self, previous_state_flags:Gtk.StateFlags)
Source code in lutris/gui/widgets/sidebar.py
def do_state_flags_changed(self, previous_flags): # pylint: disable=arguments-differ
if self.id:
self.update_buttons()
Gtk.ListBoxRow.do_state_flags_changed(self, previous_flags)
get_actions(self)
¶
Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
return []
is_row_active(self)
¶
Return true if the row is hovered or is the one selected
Source code in lutris/gui/widgets/sidebar.py
def is_row_active(self):
"""Return true if the row is hovered or is the one selected"""
flags = self.get_state_flags()
# Naming things sure is hard... But "prelight" instead of "hover"? Come on...
return flags & Gtk.StateFlags.PRELIGHT or flags & Gtk.StateFlags.SELECTED
on_realize(self, widget)
¶
Source code in lutris/gui/widgets/sidebar.py
def on_realize(self, widget):
self.create_button_box()
update_buttons(self)
¶
Source code in lutris/gui/widgets/sidebar.py
def update_buttons(self):
if self.is_updating:
self.btn_box.hide()
self.spinner.show()
self.spinner.start()
return
self.spinner.stop()
self.spinner.hide()
if self.is_row_active():
self.btn_box.show()
elif self.btn_box.get_visible():
self.btn_box.hide()
status_icon
¶
AppIndicator based tray icon
APP_INDICATOR_SUPPORTED
¶
LutrisStatusIcon
¶
Source code in lutris/gui/widgets/status_icon.py
class LutrisStatusIcon:
def __init__(self, application):
self.application = application
self.icon = self.create()
self.menu = self.get_menu()
self.set_visible(True)
if APP_INDICATOR_SUPPORTED:
self.icon.set_menu(self.menu)
else:
self.icon.connect("activate", self.on_activate)
self.icon.connect("popup-menu", self.on_menu_popup)
def create(self):
"""Create an appindicator"""
if APP_INDICATOR_SUPPORTED:
return AppIndicator.Indicator.new(
"net.lutris.Lutris", "lutris", AppIndicator.IndicatorCategory.APPLICATION_STATUS
)
return LutrisTray(self.application)
def is_visible(self):
"""Whether the icon is visible"""
if APP_INDICATOR_SUPPORTED:
return self.icon.get_status() != AppIndicator.IndicatorStatus.PASSIVE
return self.icon.is_visible()
def set_visible(self, value):
"""Set the visibility of the icon"""
if APP_INDICATOR_SUPPORTED:
if value:
visible = AppIndicator.IndicatorStatus.ACTIVE
else:
visible = AppIndicator.IndicatorStatus.ACTIVE
self.icon.set_status(visible)
else:
self.icon.set_visible(value)
def get_menu(self):
"""Instanciates the menu attached to the tray icon"""
menu = Gtk.Menu()
installed_games = self.add_games()
number_of_games_in_menu = 10
for game in installed_games[:number_of_games_in_menu]:
menu.append(self._make_menu_item_for_game(game))
menu.append(Gtk.SeparatorMenuItem())
present_menu = Gtk.ImageMenuItem()
present_menu.set_image(Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU))
present_menu.set_label(_("Show Lutris"))
present_menu.connect("activate", self.on_activate)
menu.append(present_menu)
quit_menu = Gtk.MenuItem()
quit_menu.set_label(_("Quit"))
quit_menu.connect("activate", self.on_quit_application)
menu.append(quit_menu)
menu.show_all()
return menu
def on_activate(self, _status_icon, _event=None):
"""Callback to show or hide the window"""
self.application.window.present()
def on_menu_popup(self, _status_icon, button, time):
"""Callback to show the contextual menu"""
self.menu.popup(None, None, None, None, button, time)
def on_quit_application(self, _widget):
"""Callback to quit the program"""
self.application.do_shutdown()
def _make_menu_item_for_game(self, game):
menu_item = Gtk.MenuItem()
menu_item.set_label(game["name"])
menu_item.connect("activate", self.on_game_selected, game["id"])
return menu_item
@staticmethod
def add_games():
"""Adds installed games in order of last use"""
installed_games = get_games(filters={"installed": 1})
installed_games.sort(
key=lambda game: max(game["lastplayed"] or 0, game["installed_at"] or 0),
reverse=True,
)
return installed_games
def on_game_selected(self, _widget, game_id):
Game(game_id).launch()
__init__(self, application)
special
¶
Source code in lutris/gui/widgets/status_icon.py
def __init__(self, application):
self.application = application
self.icon = self.create()
self.menu = self.get_menu()
self.set_visible(True)
if APP_INDICATOR_SUPPORTED:
self.icon.set_menu(self.menu)
else:
self.icon.connect("activate", self.on_activate)
self.icon.connect("popup-menu", self.on_menu_popup)
add_games()
staticmethod
¶
Adds installed games in order of last use
Source code in lutris/gui/widgets/status_icon.py
@staticmethod
def add_games():
"""Adds installed games in order of last use"""
installed_games = get_games(filters={"installed": 1})
installed_games.sort(
key=lambda game: max(game["lastplayed"] or 0, game["installed_at"] or 0),
reverse=True,
)
return installed_games
create(self)
¶
Create an appindicator
Source code in lutris/gui/widgets/status_icon.py
def create(self):
"""Create an appindicator"""
if APP_INDICATOR_SUPPORTED:
return AppIndicator.Indicator.new(
"net.lutris.Lutris", "lutris", AppIndicator.IndicatorCategory.APPLICATION_STATUS
)
return LutrisTray(self.application)
get_menu(self)
¶
Instanciates the menu attached to the tray icon
Source code in lutris/gui/widgets/status_icon.py
def get_menu(self):
"""Instanciates the menu attached to the tray icon"""
menu = Gtk.Menu()
installed_games = self.add_games()
number_of_games_in_menu = 10
for game in installed_games[:number_of_games_in_menu]:
menu.append(self._make_menu_item_for_game(game))
menu.append(Gtk.SeparatorMenuItem())
present_menu = Gtk.ImageMenuItem()
present_menu.set_image(Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU))
present_menu.set_label(_("Show Lutris"))
present_menu.connect("activate", self.on_activate)
menu.append(present_menu)
quit_menu = Gtk.MenuItem()
quit_menu.set_label(_("Quit"))
quit_menu.connect("activate", self.on_quit_application)
menu.append(quit_menu)
menu.show_all()
return menu
is_visible(self)
¶
Whether the icon is visible
Source code in lutris/gui/widgets/status_icon.py
def is_visible(self):
"""Whether the icon is visible"""
if APP_INDICATOR_SUPPORTED:
return self.icon.get_status() != AppIndicator.IndicatorStatus.PASSIVE
return self.icon.is_visible()
on_activate(self, _status_icon, _event=None)
¶
Callback to show or hide the window
Source code in lutris/gui/widgets/status_icon.py
def on_activate(self, _status_icon, _event=None):
"""Callback to show or hide the window"""
self.application.window.present()
on_game_selected(self, _widget, game_id)
¶
Source code in lutris/gui/widgets/status_icon.py
def on_game_selected(self, _widget, game_id):
Game(game_id).launch()
on_menu_popup(self, _status_icon, button, time)
¶
Callback to show the contextual menu
Source code in lutris/gui/widgets/status_icon.py
def on_menu_popup(self, _status_icon, button, time):
"""Callback to show the contextual menu"""
self.menu.popup(None, None, None, None, button, time)
on_quit_application(self, _widget)
¶
Callback to quit the program
Source code in lutris/gui/widgets/status_icon.py
def on_quit_application(self, _widget):
"""Callback to quit the program"""
self.application.do_shutdown()
set_visible(self, value)
¶
Set the visibility of the icon
Source code in lutris/gui/widgets/status_icon.py
def set_visible(self, value):
"""Set the visibility of the icon"""
if APP_INDICATOR_SUPPORTED:
if value:
visible = AppIndicator.IndicatorStatus.ACTIVE
else:
visible = AppIndicator.IndicatorStatus.ACTIVE
self.icon.set_status(visible)
else:
self.icon.set_visible(value)
LutrisTray (StatusIcon)
¶
Lutris tray icon
Source code in lutris/gui/widgets/status_icon.py
class LutrisTray(Gtk.StatusIcon):
"""Lutris tray icon"""
def __init__(self, application, **_kwargs):
super().__init__()
self.set_tooltip_text(_("Lutris"))
self.set_visible(True)
self.application = application
self.set_from_icon_name("lutris")
__init__(self, application, **_kwargs)
special
¶
Source code in lutris/gui/widgets/status_icon.py
def __init__(self, application, **_kwargs):
super().__init__()
self.set_tooltip_text(_("Lutris"))
self.set_visible(True)
self.application = application
self.set_from_icon_name("lutris")
utils
¶
Various utilities using the GObject framework
BANNER_SIZE
¶
ICON_SIZE
¶
convert_to_background(background_path, target_size=(320, 1080))
¶
Converts a image to a pane background
Source code in lutris/gui/widgets/utils.py
def convert_to_background(background_path, target_size=(320, 1080)):
"""Converts a image to a pane background"""
coverart = Image.open(background_path)
coverart = coverart.convert("RGBA")
target_width, target_height = target_size
image_height = int(target_height * 0.80) # 80% of the mask is visible
orig_width, orig_height = coverart.size
# Resize and crop coverart
width = int(orig_width * (image_height / orig_height))
offset = int((width - target_width) / 2)
coverart = coverart.resize((width, image_height), resample=Image.BICUBIC)
coverart = coverart.crop((offset, 0, target_width + offset, image_height))
# Resize canvas of coverart by putting transparent pixels on the bottom
coverart_bg = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 0))
coverart_bg.paste(coverart, (0, 0, target_width, image_height))
# Apply a tint to the base image
# tint = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 255))
# coverart = Image.blend(coverart, tint, 0.6)
# Paste coverart on transparent image while applying a gradient mask
background = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 0))
mask = Image.open(os.path.join(datapath.get(), "media/mask.png"))
background.paste(coverart_bg, mask=mask)
return background
get_default_icon(size)
¶
Source code in lutris/gui/widgets/utils.py
def get_default_icon(size):
if size[0] == size[1]:
return os.path.join(datapath.get(), "media/default_icon.png")
return os.path.join(datapath.get(), "media/default_banner.png")
get_icon(icon_name, icon_format='image', size=None, icon_type='runner')
¶
Return an icon based on the given name, format, size and type.
icon_name -- The name of the icon to retrieve format -- The format of the icon, which should be either 'image' or 'pixbuf' (default 'image') size -- The size for the desired image (default None) icon_type -- Retrieve either a 'runner' or 'platform' icon (default 'runner')
Source code in lutris/gui/widgets/utils.py
def get_icon(icon_name, icon_format="image", size=None, icon_type="runner"):
"""Return an icon based on the given name, format, size and type.
Keyword arguments:
icon_name -- The name of the icon to retrieve
format -- The format of the icon, which should be either 'image' or 'pixbuf' (default 'image')
size -- The size for the desired image (default None)
icon_type -- Retrieve either a 'runner' or 'platform' icon (default 'runner')
"""
filename = icon_name.lower().replace(" ", "") + ".png"
icon_path = os.path.join(settings.RUNTIME_DIR, "icons/hicolor/64x64/apps", filename)
if not os.path.exists(icon_path):
return None
if icon_format == "image":
icon = Gtk.Image()
if size:
icon.set_from_pixbuf(get_pixbuf(icon_path, size))
else:
icon.set_from_file(icon_path)
return icon
if icon_format == "pixbuf" and size:
return get_pixbuf(icon_path, size)
raise ValueError("Invalid arguments")
get_link_button(text)
¶
Return a transparent text button for the side panels
Source code in lutris/gui/widgets/utils.py
def get_link_button(text):
"""Return a transparent text button for the side panels"""
button = Gtk.Button(text, visible=True)
button.props.relief = Gtk.ReliefStyle.NONE
button.get_children()[0].set_alignment(0, 0.5)
button.get_style_context().add_class("panel-button")
button.set_size_request(-1, 24)
return button
get_main_window(widget)
¶
Return the application's main window from one of its widget
Source code in lutris/gui/widgets/utils.py
def get_main_window(widget):
"""Return the application's main window from one of its widget"""
parent = widget.get_toplevel()
if not isinstance(parent, Gtk.Window):
# The sync dialog may have closed
parent = Gio.Application.get_default().props.active_window
for window in parent.application.get_windows():
if "LutrisWindow" in window.__class__.__name__:
return window
return
get_overlay(overlay_path, size)
¶
Source code in lutris/gui/widgets/utils.py
def get_overlay(overlay_path, size):
width, height = size
transparent_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(overlay_path, width, height)
transparent_pixbuf = transparent_pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
return transparent_pixbuf
get_pixbuf(image, size, fallback=None, is_installed=True)
¶
Return a pixbuf from file image at size or fallback to fallback
Source code in lutris/gui/widgets/utils.py
def get_pixbuf(image, size, fallback=None, is_installed=True):
"""Return a pixbuf from file `image` at `size` or fallback to `fallback`"""
width, height = size
pixbuf = None
if system.path_exists(image, exclude_empty=True):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(image, width, height)
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
except GLib.GError:
logger.error("Unable to load icon from image %s", image)
else:
if not fallback:
fallback = get_default_icon(size)
if system.path_exists(fallback):
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(fallback, width, height)
if is_installed and pixbuf:
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
return pixbuf
overlay = os.path.join(datapath.get(), "media/unavailable.png")
transparent_pixbuf = get_overlay(overlay, size).copy()
if pixbuf:
pixbuf.composite(
transparent_pixbuf,
0,
0,
size[0],
size[1],
0,
0,
1,
1,
GdkPixbuf.InterpType.NEAREST,
100,
)
return transparent_pixbuf
get_stock_icon(name, size)
¶
Return a pixbuf from a stock icon name
Source code in lutris/gui/widgets/utils.py
def get_stock_icon(name, size):
"""Return a pixbuf from a stock icon name"""
theme = Gtk.IconTheme.get_default()
try:
return theme.load_icon(name, size, Gtk.IconLookupFlags.GENERIC_FALLBACK)
except GLib.GError:
logger.error("Failed to read icon %s", name)
return None
has_stock_icon(name)
¶
This tests if a GTK stock icon is known; if not we can try a fallback.
Source code in lutris/gui/widgets/utils.py
def has_stock_icon(name):
"""This tests if a GTK stock icon is known; if not we can try a fallback."""
theme = Gtk.IconTheme.get_default()
return theme.has_icon(name)
image2pixbuf(image)
¶
Converts a PIL Image to a GDK Pixbuf
Source code in lutris/gui/widgets/utils.py
def image2pixbuf(image):
"""Converts a PIL Image to a GDK Pixbuf"""
image_array = array.array('B', image.tobytes())
width, height = image.size
return GdkPixbuf.Pixbuf.new_from_data(image_array, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4)
load_icon_theme()
¶
Add the lutris icon folder to the default theme
Source code in lutris/gui/widgets/utils.py
def load_icon_theme():
"""Add the lutris icon folder to the default theme"""
icon_theme = Gtk.IconTheme.get_default()
local_theme_path = os.path.join(settings.RUNTIME_DIR, "icons")
if local_theme_path not in icon_theme.get_search_path():
icon_theme.prepend_search_path(local_theme_path)
open_uri(uri)
¶
Opens a local or remote URI with the default application
Source code in lutris/gui/widgets/utils.py
def open_uri(uri):
"""Opens a local or remote URI with the default application"""
system.reset_library_preloads()
try:
Gtk.show_uri(None, uri, Gdk.CURRENT_TIME)
except GLib.Error as ex:
logger.exception("Failed to open URI %s: %s, falling back to xdg-open", uri, ex)
system.execute(["xdg-open", uri])
paste_overlay(base_image, overlay_image, position=0.7)
¶
Source code in lutris/gui/widgets/utils.py
def paste_overlay(base_image, overlay_image, position=0.7):
base_width, base_height = base_image.size
overlay_width, overlay_height = overlay_image.size
offset_x = int((base_width - overlay_width) / 2)
offset_y = int((base_height - overlay_height) / 2)
base_image.paste(
overlay_image, (
offset_x,
offset_y,
overlay_width + offset_x,
overlay_height + offset_y
),
mask=overlay_image
)
return base_image
thumbnail_image(base_image, target_size)
¶
Source code in lutris/gui/widgets/utils.py
def thumbnail_image(base_image, target_size):
base_width, base_height = base_image.size
base_ratio = base_width / base_height
target_width, target_height = target_size
target_ratio = target_width / target_height
# Resize and crop coverart
if base_ratio >= target_ratio:
width = int(base_width * (target_height / base_height))
height = target_height
else:
width = target_width
height = int(base_height * (target_width / base_width))
x_offset = int((width - target_width) / 2)
y_offset = int((height - target_height) / 2)
base_image = base_image.resize((width, height), resample=Image.BICUBIC)
base_image = base_image.crop((x_offset, y_offset, width - x_offset, height - y_offset))
return base_image
window
¶
BaseApplicationWindow (ApplicationWindow)
¶
Window used to guide the user through a issue reporting process
Source code in lutris/gui/widgets/window.py
class BaseApplicationWindow(Gtk.ApplicationWindow):
"""Window used to guide the user through a issue reporting process"""
def __init__(self, application):
Gtk.ApplicationWindow.__init__(self, icon_name="lutris", application=application)
self.application = application
self.set_show_menubar(False)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", self.on_destroy)
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, visible=True)
self.vbox.set_margin_top(18)
self.vbox.set_margin_bottom(18)
self.vbox.set_margin_right(18)
self.vbox.set_margin_left(18)
self.add(self.vbox)
self.action_buttons = Gtk.Box(spacing=6)
self.vbox.pack_end(self.action_buttons, False, False, 0)
def get_action_button(self, label, handler=None, tooltip=None):
"""Returns a button that can be used for the action bar"""
button = Gtk.Button.new_with_mnemonic(label)
if handler:
button.connect("clicked", handler)
if tooltip:
button.set_tooltip_text(tooltip)
return button
def on_destroy(self, _widget=None, _data=None):
"""Destroy callback"""
self.destroy()
def present(self): # pylint: disable=arguments-differ
"""The base implementation doesn't always work, this one does."""
self.set_keep_above(True)
super().present()
self.set_keep_above(False)
super().present()
__init__(self, application)
special
¶
Source code in lutris/gui/widgets/window.py
def __init__(self, application):
Gtk.ApplicationWindow.__init__(self, icon_name="lutris", application=application)
self.application = application
self.set_show_menubar(False)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", self.on_destroy)
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, visible=True)
self.vbox.set_margin_top(18)
self.vbox.set_margin_bottom(18)
self.vbox.set_margin_right(18)
self.vbox.set_margin_left(18)
self.add(self.vbox)
self.action_buttons = Gtk.Box(spacing=6)
self.vbox.pack_end(self.action_buttons, False, False, 0)
get_action_button(self, label, handler=None, tooltip=None)
¶
Returns a button that can be used for the action bar
Source code in lutris/gui/widgets/window.py
def get_action_button(self, label, handler=None, tooltip=None):
"""Returns a button that can be used for the action bar"""
button = Gtk.Button.new_with_mnemonic(label)
if handler:
button.connect("clicked", handler)
if tooltip:
button.set_tooltip_text(tooltip)
return button
on_destroy(self, _widget=None, _data=None)
¶
Destroy callback
Source code in lutris/gui/widgets/window.py
def on_destroy(self, _widget=None, _data=None):
"""Destroy callback"""
self.destroy()
present(self)
¶
The base implementation doesn't always work, this one does.
Source code in lutris/gui/widgets/window.py
def present(self): # pylint: disable=arguments-differ
"""The base implementation doesn't always work, this one does."""
self.set_keep_above(True)
super().present()
self.set_keep_above(False)
super().present()
installer
special
¶
Install script interpreter package.
AUTO_ELF_EXE
¶
AUTO_WIN32_EXE
¶
get_installers(game_slug=None, installer_file=None, revision=None)
¶
Source code in lutris/installer/__init__.py
def get_installers(game_slug=None, installer_file=None, revision=None):
# check if installer is local or online
if system.path_exists(installer_file):
return read_script(installer_file)
return get_game_installers(game_slug=game_slug, revision=revision)
read_script(filename)
¶
Return scripts from a local file
Source code in lutris/installer/__init__.py
def read_script(filename):
"""Return scripts from a local file"""
logger.debug("Loading script(s) from %s", filename)
with open(filename, "r", encoding='utf-8') as local_file:
script = yaml.safe_load(local_file.read())
if isinstance(script, list):
return script
if "results" in script:
return script["results"]
return [script]
commands
¶
Commands for installer scripts
CommandsMixin
¶
The directives for the installer: part of the install script.
Source code in lutris/installer/commands.py
class CommandsMixin:
"""The directives for the `installer:` part of the install script."""
def __init__(self):
if isinstance(self, CommandsMixin):
raise RuntimeError("This class is a mixin")
def _get_runner_version(self):
"""Return the version of the runner used for the installer"""
if self.installer.runner == "wine":
# If a version is specified in the script choose this one
if self.installer.script.get(self.installer.runner):
return self.installer.script[self.installer.runner].get("version")
# If the installer is a extension, use the wine version from the base game
if self.installer.requires:
db_game = get_game_by_field(self.installer.requires, field="installer_slug")
if not db_game:
db_game = get_game_by_field(self.installer.requires, field="slug")
if not db_game:
logger.warning("Can't find game %s", self.installer.requires)
return None
game = Game(db_game["id"])
return game.config.runner_config["version"]
if self.installer.runner == "libretro":
return self.installer.script["game"]["core"]
return None
@staticmethod
def _check_required_params(params, command_data, command_name):
"""Verify presence of a list of parameters required by a command."""
if isinstance(params, str):
params = [params]
for param in params:
if isinstance(param, tuple):
param_present = False
for key in param:
if key in command_data:
param_present = True
if not param_present:
raise ScriptingError(
_("One of {params} parameter is mandatory for the {cmd} command").format(
params=_(" or ").join(param), cmd=command_name),
command_data,
)
else:
if param not in command_data:
raise ScriptingError(
_("The {param} parameter is mandatory for the {cmd} command").format(
param=param, cmd=command_name),
command_data,
)
@staticmethod
def _is_cached_file(file_path):
"""Return whether a file referenced by file_id is stored in the cache"""
pga_cache_path = get_cache_path()
if not pga_cache_path:
return False
return file_path.startswith(pga_cache_path)
def chmodx(self, filename):
"""Make filename executable"""
filename = self._substitute(filename)
if not system.path_exists(filename):
raise ScriptingError(_("Invalid file '%s'. Can't make it executable") % filename)
system.make_executable(filename)
def execute(self, data):
"""Run an executable file."""
args = []
terminal = None
working_dir = None
env = {}
if isinstance(data, dict):
self._check_required_params([("file", "command")], data, "execute")
if "command" in data and "file" in data:
raise ScriptingError(
_("Parameters file and command can't be used "
"at the same time for the execute command"),
data,
)
# Accept return codes other than 0
if "return_code" in data:
return_code = data.pop("return_code")
else:
return_code = "0"
exec_path = data.get("file", "")
command = data.get("command", "")
args_string = data.get("args", "")
for arg in shlex.split(args_string):
args.append(self._substitute(arg))
terminal = data.get("terminal")
working_dir = data.get("working_dir")
if not data.get("disable_runtime"):
# Possibly need to handle prefer_system_libs here
env.update(runtime.get_env())
# Loading environment variables set in the script
env.update(self.script_env)
# Environment variables can also be passed to the execute command
local_env = data.get("env") or {}
env.update({key: self._substitute(value) for key, value in local_env.items()})
include_processes = shlex.split(data.get("include_processes", ""))
exclude_processes = shlex.split(data.get("exclude_processes", ""))
elif isinstance(data, str):
command = data
include_processes = []
exclude_processes = []
else:
raise ScriptingError(_("No parameters supplied to execute command."), data)
if command:
exec_path = "bash"
args = ["-c", self._get_file_path(command.strip())]
include_processes.append("bash")
else:
# Determine whether 'file' value is a file id or a path
exec_path = self._get_file_path(exec_path)
if system.path_exists(exec_path) and not system.is_executable(exec_path):
logger.warning("Making %s executable", exec_path)
system.make_executable(exec_path)
exec_abs_path = system.find_executable(exec_path)
if not exec_abs_path:
raise ScriptingError(_("Unable to find executable %s") % exec_path)
if terminal:
terminal = linux.get_default_terminal()
if not working_dir or not os.path.exists(working_dir):
working_dir = self.target_path
command = MonitoredCommand(
[exec_abs_path] + args,
env=env,
term=terminal,
cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes,
)
command.accepted_return_code = return_code
command.start()
GLib.idle_add(self.parent.attach_logger, command)
self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
return "STOP"
def extract(self, data):
"""Extract a file, guessing the compression method."""
self._check_required_params([("file", "src")], data, "extract")
src_param = data.get("file") or data.get("src")
filespec = self._get_file_path(src_param)
if os.path.exists(filespec):
filenames = [filespec]
else:
filenames = glob.glob(filespec)
if not filenames:
raise ScriptingError(_("%s does not exist") % filespec)
if "dst" in data:
dest_path = self._substitute(data["dst"])
else:
dest_path = self.target_path
for filename in filenames:
msg = _("Extracting %s") % os.path.basename(filename)
logger.debug(msg)
GLib.idle_add(self.parent.set_status, msg)
merge_single = "nomerge" not in data
extractor = data.get("format")
logger.debug("extracting file %s to %s", filename, dest_path)
self._killable_process(extract.extract_archive, filename, dest_path, merge_single, extractor)
logger.debug("Extract done")
def input_menu(self, data):
"""Display an input request as a dropdown menu with options."""
self._check_required_params("options", data, "input_menu")
identifier = data.get("id")
alias = "INPUT_%s" % identifier if identifier else None
has_entry = data.get("entry")
options = data["options"]
preselect = self._substitute(data.get("preselect", ""))
GLib.idle_add(
self.parent.input_menu,
alias,
options,
preselect,
has_entry,
self._on_input_menu_validated,
)
return "STOP"
def _on_input_menu_validated(self, _widget, *args):
alias = args[0]
menu = args[1]
choosen_option = menu.get_active_id()
if choosen_option:
self.user_inputs.append({"alias": alias, "value": choosen_option})
GLib.idle_add(self.parent.continue_button.hide)
self._iter_commands()
def insert_disc(self, data):
"""Request user to insert an optical disc"""
self._check_required_params("requires", data, "insert_disc")
requires = data.get("requires")
message = data.get(
"message",
_("Insert or mount game disc and click Autodetect or\n"
"use Browse if the disc is mounted on a non standard location."),
)
message += (
_("\n\nLutris is looking for a mounted disk drive or image \n"
"containing the following file or folder:\n"
"<i>%s</i>") % requires
)
if self.installer.runner == "wine":
GLib.idle_add(self.parent.eject_button.show)
GLib.idle_add(self.parent.ask_for_disc, message, self._find_matching_disc, requires)
return "STOP"
def _find_matching_disc(self, _widget, requires, extra_path=None):
if extra_path:
drives = [extra_path]
else:
drives = system.get_mounted_discs()
for drive in drives:
required_abspath = os.path.join(drive, requires)
required_abspath = system.fix_path_case(required_abspath)
if required_abspath and system.path_exists(required_abspath):
logger.debug("Found %s on cdrom %s", requires, drive)
self.game_disc = drive
self._iter_commands()
break
def mkdir(self, directory):
"""Create directory"""
directory = self._substitute(directory)
try:
os.makedirs(directory)
except OSError:
logger.debug("Directory %s already exists", directory)
else:
logger.debug("Created directory %s", directory)
def merge(self, params):
"""Merge the contents given by src to destination folder dst"""
self._check_required_params(["src", "dst"], params, "merge")
src, dst = self._get_move_paths(params)
logger.debug("Merging %s into %s", src, dst)
if not os.path.exists(src):
if params.get("optional"):
logger.info("Optional path %s not present", src)
return
raise ScriptingError(_("Source does not exist: %s") % src, params)
os.makedirs(dst, exist_ok=True)
if os.path.isfile(src):
# If single file, copy it and change reference in game file so it
# can be used as executable. Skip copying if the source is the same
# as destination.
if os.path.dirname(src) != dst:
self._killable_process(shutil.copy, src, dst)
if params["src"] in self.game_files.keys():
self.game_files[params["src"]] = os.path.join(dst, os.path.basename(src))
return
self._killable_process(system.merge_folders, src, dst)
def copy(self, params):
"""Alias for merge"""
self.merge(params)
def move(self, params):
"""Move a file or directory into a destination folder."""
self._check_required_params(["src", "dst"], params, "move")
src, dst = self._get_move_paths(params)
logger.debug("Moving %s to %s", src, dst)
if not os.path.exists(src):
if params.get("optional"):
logger.info("Optional path %s not present", src)
return
raise ScriptingError(_("Invalid source for 'move' operation: %s") % src)
if os.path.isfile(src):
if os.path.dirname(src) == dst:
logger.info("Source file is the same as destination, skipping")
return
if os.path.exists(os.path.join(dst, os.path.basename(src))):
# May not be the best choice, but it's the safest.
# Maybe should display confirmation dialog (Overwrite / Skip) ?
logger.info("Destination file exists, skipping")
return
try:
if self._is_cached_file(src):
action = shutil.copy
else:
action = shutil.move
self._killable_process(action, src, dst)
except shutil.Error as err:
raise ScriptingError(_("Can't move {src} \nto destination {dst}").format(src=src, dst=dst)) from err
def rename(self, params):
"""Rename file or folder."""
self._check_required_params(["src", "dst"], params, "rename")
src, dst = self._get_move_paths(params)
if not os.path.exists(src):
raise ScriptingError(_("Rename error, source path does not exist: %s") % src)
if os.path.isdir(dst):
try:
os.rmdir(dst) # Remove if empty
except OSError:
pass
if os.path.exists(dst):
raise ScriptingError(_("Rename error, destination already exists: %s") % src)
dst_dir = os.path.dirname(dst)
# Pre-move on dest filesystem to avoid error with
# os.rename through different filesystems
temp_dir = os.path.join(dst_dir, "lutris_rename_temp")
os.makedirs(temp_dir)
self._killable_process(shutil.move, src, temp_dir)
src = os.path.join(temp_dir, os.path.basename(src))
os.renames(src, dst)
def _get_move_paths(self, params):
"""Process raw 'src' and 'dst' data."""
try:
src_ref = params["src"]
except KeyError as err:
raise ScriptingError(_("Missing parameter src")) from err
src = self.game_files.get(src_ref) or self._substitute(src_ref)
if not src:
raise ScriptingError(_("Wrong value for 'src' param"), src_ref)
dst_ref = params["dst"]
dst = self._substitute(dst_ref)
if not dst:
raise ScriptingError(_("Wrong value for 'dst' param"), dst_ref)
return src.rstrip("/"), dst.rstrip("/")
def substitute_vars(self, data):
"""Subsitute variable names found in given file."""
self._check_required_params("file", data, "substitute_vars")
filename = self._substitute(data["file"])
logger.debug("Substituting variables for file %s", filename)
tmp_filename = filename + ".tmp"
with open(filename, "r", encoding='utf-8') as source_file:
with open(tmp_filename, "w", encoding='utf-8') as dest_file:
line = "."
while line:
line = source_file.readline()
line = self._substitute(line)
dest_file.write(line)
os.rename(tmp_filename, filename)
def _get_task_runner_and_name(self, task_name):
if "." in task_name:
# Run a task from a different runner
# than the one for this installer
runner_name, task_name = task_name.split(".")
else:
runner_name = self.installer.runner
return runner_name, task_name
def get_wine_path(self):
"""Return absolute path of wine version used during the install"""
return get_wine_version_exe(self._get_runner_version())
def task(self, data):
"""Directive triggering another function specific to a runner.
The 'name' parameter is mandatory. If 'args' is provided it will be
passed to the runner task.
"""
self._check_required_params("name", data, "task")
if self.parent:
GLib.idle_add(self.parent.cancel_button.set_sensitive, False)
runner_name, task_name = self._get_task_runner_and_name(data.pop("name"))
# Accept return codes other than 0
if "return_code" in data:
return_code = data.pop("return_code")
else:
return_code = "0"
if runner_name.startswith("wine"):
wine_path = self.get_wine_path()
if wine_path:
data["wine_path"] = wine_path
data["prefix"] = data.get("prefix") \
or self.installer.script.get("game", {}).get("prefix") \
or "$GAMEDIR"
data["arch"] = data.get("arch") \
or self.installer.script.get("game", {}).get("arch") \
or WINE_DEFAULT_ARCH
if task_name == "wineexec":
data["env"] = self.script_env
for key in data:
value = data[key]
if isinstance(value, dict):
for inner_key in value:
value[inner_key] = self._substitute(value[inner_key])
elif isinstance(value, list):
for index, elem in enumerate(value):
value[index] = self._substitute(elem)
else:
value = self._substitute(data[key])
data[key] = value
task = import_task(runner_name, task_name)
command = task(**data)
if command:
command.accepted_return_code = return_code
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
if isinstance(command, MonitoredCommand):
# Monitor thread and continue when task has executed
GLib.idle_add(self.parent.attach_logger, command)
self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
return "STOP"
return None
def _monitor_task(self, command):
if not command.is_running:
logger.debug("Return code: %s", command.return_code)
if command.return_code not in (command.accepted_return_code, "0"):
raise ScriptingError(_("Command exited with code %s") % command.return_code)
self._iter_commands()
return False
return True
def write_file(self, params):
"""Write text to a file."""
self._check_required_params(["file", "content"], params, "write_file")
# Get file
dest_file_path = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(dest_file_path)
os.makedirs(basedir, exist_ok=True)
mode = params.get("mode", "w")
if not mode.startswith(("a", "w")):
raise ScriptingError(_("Wrong value for write_file mode: '%s'") % mode)
with open(dest_file_path, mode, encoding='utf-8') as dest_file:
dest_file.write(self._substitute(params["content"]))
def write_json(self, params):
"""Write data into a json file."""
self._check_required_params(["file", "data"], params, "write_json")
# Get file
filename = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(filename)
os.makedirs(basedir, exist_ok=True)
merge = params.get("merge", True)
# create an empty file if it doesn't exist
Path(filename).touch(exist_ok=True)
with open(filename, "r+" if merge else "w", encoding='utf-8') as json_file:
json_data = {}
if merge:
try:
json_data = json.load(json_file)
except ValueError:
logger.error("Failed to parse JSON from file %s", filename)
json_data = selective_merge(json_data, params.get("data", {}))
json_file.seek(0)
json_file.write(json.dumps(json_data, indent=2))
def write_config(self, params):
"""Write a key-value pair into an INI type config file."""
if params.get("data"):
self._check_required_params(["file", "data"], params, "write_config")
else:
self._check_required_params(["file", "section", "key", "value"], params, "write_config")
# Get file
config_file_path = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(config_file_path)
os.makedirs(basedir, exist_ok=True)
merge = params.get("merge", True)
parser = EvilConfigParser(allow_no_value=True, dict_type=MultiOrderedDict, strict=False)
parser.optionxform = str # Preserve text case
if merge:
parser.read(config_file_path)
data = {}
if params.get("data"):
data = params["data"]
else:
data[params["section"]] = {}
data[params["section"]][params["key"]] = params["value"]
for section, keys in data.items():
if not parser.has_section(section):
parser.add_section(section)
for key, value in keys.items():
value = self._substitute(value)
parser.set(section, key, value)
with open(config_file_path, "wb") as config_file:
parser.write(config_file)
def _get_file_path(self, fileid):
file_path = self.game_files.get(fileid)
if not file_path:
file_path = self._substitute(fileid)
return file_path
def _killable_process(self, func, *args, **kwargs):
"""Run function `func` in a separate, killable process."""
with multiprocessing.Pool(1) as process:
result_obj = process.apply_async(func, args, kwargs)
self.abort_current_task = process.terminate
result = result_obj.get() # Wait process end & re-raise exceptions
self.abort_current_task = None
logger.debug("Process %s returned: %s", func, result)
return result
def _extract_gog_game(self, file_id):
self.extract({
"src": file_id,
"dst": "$GAMEDIR",
"extractor": "innoextract"
})
app_path = os.path.join(self.target_path, "app")
if system.path_exists(app_path):
for app_content in os.listdir(app_path):
source_path = os.path.join(app_path, app_content)
if os.path.exists(os.path.join(self.target_path, app_content)):
self.merge({"src": source_path, "dst": self.target_path})
else:
self.move({"src": source_path, "dst": self.target_path})
support_path = os.path.join(self.target_path, "__support/app")
if system.path_exists(support_path):
self.merge({"src": support_path, "dst": self.target_path})
def _get_scummvm_arguments(self, gog_config_path):
"""Return a ScummVM configuration from the GOG config files"""
with open(gog_config_path, encoding='utf-8') as gog_config_file:
gog_config = json.loads(gog_config_file.read())
game_tasks = [task for task in gog_config["playTasks"] if task["category"] == "game"]
arguments = game_tasks[0]["arguments"]
game_id = arguments.split()[-1]
arguments = " ".join(arguments.split()[:-1])
base_dir = os.path.dirname(gog_config_path)
return {
"game_id": game_id,
"path": base_dir,
"arguments": arguments
}
def autosetup_gog_game(self, file_id, silent=False):
"""Automatically guess the best way to install a GOG game by inspecting its contents.
This chooses the right runner (DOSBox, Wine) for Windows game files.
Linux setup files don't use innosetup, they can be unzipped instead.
"""
file_path = self.game_files[file_id]
file_list = extract.get_innoextract_list(file_path)
dosbox_found = False
scummvm_found = False
windows_override_found = False # DOS games that also have a Windows executable
for filename in file_list:
if "dosbox/dosbox.exe" in filename.lower():
dosbox_found = True
if "scummvm/scummvm.exe" in filename.lower():
scummvm_found = True
if "_some_windows.exe" in filename.lower():
# There's not a good way to handle exceptions without extracting the .info file
# before extracting the game. Added for Quake but GlQuake.exe doesn't run on modern wine
windows_override_found = True
if dosbox_found and not windows_override_found:
self._extract_gog_game(file_id)
dosbox_config = {
"working_dir": "$GAMEDIR/DOSBOX",
}
for filename in os.listdir(self.target_path):
if filename.endswith("_single.conf"):
dosbox_config["main_file"] = filename
elif filename.endswith(".conf"):
dosbox_config["config_file"] = filename
self.installer.script["game"] = dosbox_config
self.installer.runner = "dosbox"
elif scummvm_found:
self._extract_gog_game(file_id)
arguments = None
for filename in os.listdir(self.target_path):
if filename.startswith("goggame") and filename.endswith(".info"):
arguments = self._get_scummvm_arguments(os.path.join(self.target_path, filename))
if not arguments:
raise RuntimeError("Unable to get ScummVM arguments")
logger.info("ScummVM config: %s", arguments)
self.installer.script["game"] = arguments
self.installer.runner = "scummvm"
else:
args = "/SP- /NOCANCEL"
if silent:
args += " /SUPPRESSMSGBOXES /VERYSILENT /NOGUI"
self.installer.is_gog = True
return self.task({
"name": "wineexec",
"prefix": "$GAMEDIR",
"executable": file_id,
"args": args
})
__init__(self)
special
¶
Source code in lutris/installer/commands.py
def __init__(self):
if isinstance(self, CommandsMixin):
raise RuntimeError("This class is a mixin")
autosetup_gog_game(self, file_id, silent=False)
¶
Automatically guess the best way to install a GOG game by inspecting its contents. This chooses the right runner (DOSBox, Wine) for Windows game files. Linux setup files don't use innosetup, they can be unzipped instead.
Source code in lutris/installer/commands.py
def autosetup_gog_game(self, file_id, silent=False):
"""Automatically guess the best way to install a GOG game by inspecting its contents.
This chooses the right runner (DOSBox, Wine) for Windows game files.
Linux setup files don't use innosetup, they can be unzipped instead.
"""
file_path = self.game_files[file_id]
file_list = extract.get_innoextract_list(file_path)
dosbox_found = False
scummvm_found = False
windows_override_found = False # DOS games that also have a Windows executable
for filename in file_list:
if "dosbox/dosbox.exe" in filename.lower():
dosbox_found = True
if "scummvm/scummvm.exe" in filename.lower():
scummvm_found = True
if "_some_windows.exe" in filename.lower():
# There's not a good way to handle exceptions without extracting the .info file
# before extracting the game. Added for Quake but GlQuake.exe doesn't run on modern wine
windows_override_found = True
if dosbox_found and not windows_override_found:
self._extract_gog_game(file_id)
dosbox_config = {
"working_dir": "$GAMEDIR/DOSBOX",
}
for filename in os.listdir(self.target_path):
if filename.endswith("_single.conf"):
dosbox_config["main_file"] = filename
elif filename.endswith(".conf"):
dosbox_config["config_file"] = filename
self.installer.script["game"] = dosbox_config
self.installer.runner = "dosbox"
elif scummvm_found:
self._extract_gog_game(file_id)
arguments = None
for filename in os.listdir(self.target_path):
if filename.startswith("goggame") and filename.endswith(".info"):
arguments = self._get_scummvm_arguments(os.path.join(self.target_path, filename))
if not arguments:
raise RuntimeError("Unable to get ScummVM arguments")
logger.info("ScummVM config: %s", arguments)
self.installer.script["game"] = arguments
self.installer.runner = "scummvm"
else:
args = "/SP- /NOCANCEL"
if silent:
args += " /SUPPRESSMSGBOXES /VERYSILENT /NOGUI"
self.installer.is_gog = True
return self.task({
"name": "wineexec",
"prefix": "$GAMEDIR",
"executable": file_id,
"args": args
})
chmodx(self, filename)
¶
Make filename executable
Source code in lutris/installer/commands.py
def chmodx(self, filename):
"""Make filename executable"""
filename = self._substitute(filename)
if not system.path_exists(filename):
raise ScriptingError(_("Invalid file '%s'. Can't make it executable") % filename)
system.make_executable(filename)
copy(self, params)
¶
Alias for merge
Source code in lutris/installer/commands.py
def copy(self, params):
"""Alias for merge"""
self.merge(params)
execute(self, data)
¶
Run an executable file.
Source code in lutris/installer/commands.py
def execute(self, data):
"""Run an executable file."""
args = []
terminal = None
working_dir = None
env = {}
if isinstance(data, dict):
self._check_required_params([("file", "command")], data, "execute")
if "command" in data and "file" in data:
raise ScriptingError(
_("Parameters file and command can't be used "
"at the same time for the execute command"),
data,
)
# Accept return codes other than 0
if "return_code" in data:
return_code = data.pop("return_code")
else:
return_code = "0"
exec_path = data.get("file", "")
command = data.get("command", "")
args_string = data.get("args", "")
for arg in shlex.split(args_string):
args.append(self._substitute(arg))
terminal = data.get("terminal")
working_dir = data.get("working_dir")
if not data.get("disable_runtime"):
# Possibly need to handle prefer_system_libs here
env.update(runtime.get_env())
# Loading environment variables set in the script
env.update(self.script_env)
# Environment variables can also be passed to the execute command
local_env = data.get("env") or {}
env.update({key: self._substitute(value) for key, value in local_env.items()})
include_processes = shlex.split(data.get("include_processes", ""))
exclude_processes = shlex.split(data.get("exclude_processes", ""))
elif isinstance(data, str):
command = data
include_processes = []
exclude_processes = []
else:
raise ScriptingError(_("No parameters supplied to execute command."), data)
if command:
exec_path = "bash"
args = ["-c", self._get_file_path(command.strip())]
include_processes.append("bash")
else:
# Determine whether 'file' value is a file id or a path
exec_path = self._get_file_path(exec_path)
if system.path_exists(exec_path) and not system.is_executable(exec_path):
logger.warning("Making %s executable", exec_path)
system.make_executable(exec_path)
exec_abs_path = system.find_executable(exec_path)
if not exec_abs_path:
raise ScriptingError(_("Unable to find executable %s") % exec_path)
if terminal:
terminal = linux.get_default_terminal()
if not working_dir or not os.path.exists(working_dir):
working_dir = self.target_path
command = MonitoredCommand(
[exec_abs_path] + args,
env=env,
term=terminal,
cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes,
)
command.accepted_return_code = return_code
command.start()
GLib.idle_add(self.parent.attach_logger, command)
self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
return "STOP"
extract(self, data)
¶
Extract a file, guessing the compression method.
Source code in lutris/installer/commands.py
def extract(self, data):
"""Extract a file, guessing the compression method."""
self._check_required_params([("file", "src")], data, "extract")
src_param = data.get("file") or data.get("src")
filespec = self._get_file_path(src_param)
if os.path.exists(filespec):
filenames = [filespec]
else:
filenames = glob.glob(filespec)
if not filenames:
raise ScriptingError(_("%s does not exist") % filespec)
if "dst" in data:
dest_path = self._substitute(data["dst"])
else:
dest_path = self.target_path
for filename in filenames:
msg = _("Extracting %s") % os.path.basename(filename)
logger.debug(msg)
GLib.idle_add(self.parent.set_status, msg)
merge_single = "nomerge" not in data
extractor = data.get("format")
logger.debug("extracting file %s to %s", filename, dest_path)
self._killable_process(extract.extract_archive, filename, dest_path, merge_single, extractor)
logger.debug("Extract done")
get_wine_path(self)
¶
Return absolute path of wine version used during the install
Source code in lutris/installer/commands.py
def get_wine_path(self):
"""Return absolute path of wine version used during the install"""
return get_wine_version_exe(self._get_runner_version())
input_menu(self, data)
¶
Display an input request as a dropdown menu with options.
Source code in lutris/installer/commands.py
def input_menu(self, data):
"""Display an input request as a dropdown menu with options."""
self._check_required_params("options", data, "input_menu")
identifier = data.get("id")
alias = "INPUT_%s" % identifier if identifier else None
has_entry = data.get("entry")
options = data["options"]
preselect = self._substitute(data.get("preselect", ""))
GLib.idle_add(
self.parent.input_menu,
alias,
options,
preselect,
has_entry,
self._on_input_menu_validated,
)
return "STOP"
insert_disc(self, data)
¶
Request user to insert an optical disc
Source code in lutris/installer/commands.py
def insert_disc(self, data):
"""Request user to insert an optical disc"""
self._check_required_params("requires", data, "insert_disc")
requires = data.get("requires")
message = data.get(
"message",
_("Insert or mount game disc and click Autodetect or\n"
"use Browse if the disc is mounted on a non standard location."),
)
message += (
_("\n\nLutris is looking for a mounted disk drive or image \n"
"containing the following file or folder:\n"
"<i>%s</i>") % requires
)
if self.installer.runner == "wine":
GLib.idle_add(self.parent.eject_button.show)
GLib.idle_add(self.parent.ask_for_disc, message, self._find_matching_disc, requires)
return "STOP"
merge(self, params)
¶
Merge the contents given by src to destination folder dst
Source code in lutris/installer/commands.py
def merge(self, params):
"""Merge the contents given by src to destination folder dst"""
self._check_required_params(["src", "dst"], params, "merge")
src, dst = self._get_move_paths(params)
logger.debug("Merging %s into %s", src, dst)
if not os.path.exists(src):
if params.get("optional"):
logger.info("Optional path %s not present", src)
return
raise ScriptingError(_("Source does not exist: %s") % src, params)
os.makedirs(dst, exist_ok=True)
if os.path.isfile(src):
# If single file, copy it and change reference in game file so it
# can be used as executable. Skip copying if the source is the same
# as destination.
if os.path.dirname(src) != dst:
self._killable_process(shutil.copy, src, dst)
if params["src"] in self.game_files.keys():
self.game_files[params["src"]] = os.path.join(dst, os.path.basename(src))
return
self._killable_process(system.merge_folders, src, dst)
mkdir(self, directory)
¶
Create directory
Source code in lutris/installer/commands.py
def mkdir(self, directory):
"""Create directory"""
directory = self._substitute(directory)
try:
os.makedirs(directory)
except OSError:
logger.debug("Directory %s already exists", directory)
else:
logger.debug("Created directory %s", directory)
move(self, params)
¶
Move a file or directory into a destination folder.
Source code in lutris/installer/commands.py
def move(self, params):
"""Move a file or directory into a destination folder."""
self._check_required_params(["src", "dst"], params, "move")
src, dst = self._get_move_paths(params)
logger.debug("Moving %s to %s", src, dst)
if not os.path.exists(src):
if params.get("optional"):
logger.info("Optional path %s not present", src)
return
raise ScriptingError(_("Invalid source for 'move' operation: %s") % src)
if os.path.isfile(src):
if os.path.dirname(src) == dst:
logger.info("Source file is the same as destination, skipping")
return
if os.path.exists(os.path.join(dst, os.path.basename(src))):
# May not be the best choice, but it's the safest.
# Maybe should display confirmation dialog (Overwrite / Skip) ?
logger.info("Destination file exists, skipping")
return
try:
if self._is_cached_file(src):
action = shutil.copy
else:
action = shutil.move
self._killable_process(action, src, dst)
except shutil.Error as err:
raise ScriptingError(_("Can't move {src} \nto destination {dst}").format(src=src, dst=dst)) from err
rename(self, params)
¶
Rename file or folder.
Source code in lutris/installer/commands.py
def rename(self, params):
"""Rename file or folder."""
self._check_required_params(["src", "dst"], params, "rename")
src, dst = self._get_move_paths(params)
if not os.path.exists(src):
raise ScriptingError(_("Rename error, source path does not exist: %s") % src)
if os.path.isdir(dst):
try:
os.rmdir(dst) # Remove if empty
except OSError:
pass
if os.path.exists(dst):
raise ScriptingError(_("Rename error, destination already exists: %s") % src)
dst_dir = os.path.dirname(dst)
# Pre-move on dest filesystem to avoid error with
# os.rename through different filesystems
temp_dir = os.path.join(dst_dir, "lutris_rename_temp")
os.makedirs(temp_dir)
self._killable_process(shutil.move, src, temp_dir)
src = os.path.join(temp_dir, os.path.basename(src))
os.renames(src, dst)
substitute_vars(self, data)
¶
Subsitute variable names found in given file.
Source code in lutris/installer/commands.py
def substitute_vars(self, data):
"""Subsitute variable names found in given file."""
self._check_required_params("file", data, "substitute_vars")
filename = self._substitute(data["file"])
logger.debug("Substituting variables for file %s", filename)
tmp_filename = filename + ".tmp"
with open(filename, "r", encoding='utf-8') as source_file:
with open(tmp_filename, "w", encoding='utf-8') as dest_file:
line = "."
while line:
line = source_file.readline()
line = self._substitute(line)
dest_file.write(line)
os.rename(tmp_filename, filename)
task(self, data)
¶
Directive triggering another function specific to a runner.
The 'name' parameter is mandatory. If 'args' is provided it will be passed to the runner task.
Source code in lutris/installer/commands.py
def task(self, data):
"""Directive triggering another function specific to a runner.
The 'name' parameter is mandatory. If 'args' is provided it will be
passed to the runner task.
"""
self._check_required_params("name", data, "task")
if self.parent:
GLib.idle_add(self.parent.cancel_button.set_sensitive, False)
runner_name, task_name = self._get_task_runner_and_name(data.pop("name"))
# Accept return codes other than 0
if "return_code" in data:
return_code = data.pop("return_code")
else:
return_code = "0"
if runner_name.startswith("wine"):
wine_path = self.get_wine_path()
if wine_path:
data["wine_path"] = wine_path
data["prefix"] = data.get("prefix") \
or self.installer.script.get("game", {}).get("prefix") \
or "$GAMEDIR"
data["arch"] = data.get("arch") \
or self.installer.script.get("game", {}).get("arch") \
or WINE_DEFAULT_ARCH
if task_name == "wineexec":
data["env"] = self.script_env
for key in data:
value = data[key]
if isinstance(value, dict):
for inner_key in value:
value[inner_key] = self._substitute(value[inner_key])
elif isinstance(value, list):
for index, elem in enumerate(value):
value[index] = self._substitute(elem)
else:
value = self._substitute(data[key])
data[key] = value
task = import_task(runner_name, task_name)
command = task(**data)
if command:
command.accepted_return_code = return_code
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
if isinstance(command, MonitoredCommand):
# Monitor thread and continue when task has executed
GLib.idle_add(self.parent.attach_logger, command)
self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
return "STOP"
return None
write_config(self, params)
¶
Write a key-value pair into an INI type config file.
Source code in lutris/installer/commands.py
def write_config(self, params):
"""Write a key-value pair into an INI type config file."""
if params.get("data"):
self._check_required_params(["file", "data"], params, "write_config")
else:
self._check_required_params(["file", "section", "key", "value"], params, "write_config")
# Get file
config_file_path = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(config_file_path)
os.makedirs(basedir, exist_ok=True)
merge = params.get("merge", True)
parser = EvilConfigParser(allow_no_value=True, dict_type=MultiOrderedDict, strict=False)
parser.optionxform = str # Preserve text case
if merge:
parser.read(config_file_path)
data = {}
if params.get("data"):
data = params["data"]
else:
data[params["section"]] = {}
data[params["section"]][params["key"]] = params["value"]
for section, keys in data.items():
if not parser.has_section(section):
parser.add_section(section)
for key, value in keys.items():
value = self._substitute(value)
parser.set(section, key, value)
with open(config_file_path, "wb") as config_file:
parser.write(config_file)
write_file(self, params)
¶
Write text to a file.
Source code in lutris/installer/commands.py
def write_file(self, params):
"""Write text to a file."""
self._check_required_params(["file", "content"], params, "write_file")
# Get file
dest_file_path = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(dest_file_path)
os.makedirs(basedir, exist_ok=True)
mode = params.get("mode", "w")
if not mode.startswith(("a", "w")):
raise ScriptingError(_("Wrong value for write_file mode: '%s'") % mode)
with open(dest_file_path, mode, encoding='utf-8') as dest_file:
dest_file.write(self._substitute(params["content"]))
write_json(self, params)
¶
Write data into a json file.
Source code in lutris/installer/commands.py
def write_json(self, params):
"""Write data into a json file."""
self._check_required_params(["file", "data"], params, "write_json")
# Get file
filename = self._get_file_path(params["file"])
# Create dir if necessary
basedir = os.path.dirname(filename)
os.makedirs(basedir, exist_ok=True)
merge = params.get("merge", True)
# create an empty file if it doesn't exist
Path(filename).touch(exist_ok=True)
with open(filename, "r+" if merge else "w", encoding='utf-8') as json_file:
json_data = {}
if merge:
try:
json_data = json.load(json_file)
except ValueError:
logger.error("Failed to parse JSON from file %s", filename)
json_data = selective_merge(json_data, params.get("data", {}))
json_file.seek(0)
json_file.write(json.dumps(json_data, indent=2))
errors
¶
Installer specific exceptions
FileNotAvailable (Exception)
¶
Raised when a file has to be provided by the user
Source code in lutris/installer/errors.py
class FileNotAvailable(Exception):
"""Raised when a file has to be provided by the user"""
MissingGameDependency (Exception)
¶
Raise when a game requires another game that isn't installed
Source code in lutris/installer/errors.py
class MissingGameDependency(Exception):
"""Raise when a game requires another game that isn't installed"""
def __init__(self, slug=None):
self.slug = slug
super().__init__()
__init__(self, slug=None)
special
¶
Source code in lutris/installer/errors.py
def __init__(self, slug=None):
self.slug = slug
super().__init__()
ScriptingError (Exception)
¶
Custom exception for scripting errors, can be caught by modifying excepthook.
Source code in lutris/installer/errors.py
class ScriptingError(Exception):
"""Custom exception for scripting errors, can be caught by modifying
excepthook."""
def __init__(self, message, faulty_data=None):
self.message = message
self.faulty_data = faulty_data
super().__init__()
logger.error(self.__str__())
def __str__(self):
faulty_data = repr(self.faulty_data)
return self.message + "\n%s" % faulty_data if faulty_data else ""
def __repr__(self):
return self.message
__init__(self, message, faulty_data=None)
special
¶
Source code in lutris/installer/errors.py
def __init__(self, message, faulty_data=None):
self.message = message
self.faulty_data = faulty_data
super().__init__()
logger.error(self.__str__())
__repr__(self)
special
¶
Source code in lutris/installer/errors.py
def __repr__(self):
return self.message
__str__(self)
special
¶
Source code in lutris/installer/errors.py
def __str__(self):
faulty_data = repr(self.faulty_data)
return self.message + "\n%s" % faulty_data if faulty_data else ""
error_handler(error_type, value, traceback)
¶
Intercept all possible exceptions and raise them as ScriptingErrors
Source code in lutris/installer/errors.py
def error_handler(error_type, value, traceback):
"""Intercept all possible exceptions and raise them as ScriptingErrors"""
if error_type == ScriptingError:
message = value.message
if value.faulty_data:
message += "\n<b>%s</b>" % gtk_safe(value.faulty_data)
ErrorDialog(message)
else:
_excepthook(error_type, value, traceback)
installer
¶
Lutris installer class
LutrisInstaller
¶
Represents a Lutris installer
Source code in lutris/installer/installer.py
class LutrisInstaller: # pylint: disable=too-many-instance-attributes
"""Represents a Lutris installer"""
def __init__(self, installer, interpreter, service, appid):
self.interpreter = interpreter
self.installer = installer
self.is_update = False
self.version = installer["version"]
self.slug = installer["slug"]
self.year = installer.get("year")
self.runner = installer["runner"]
self.script = installer.get("script")
self.game_name = installer["name"]
self.game_slug = installer["game_slug"]
self.service = self.get_service(initial=service)
self.service_appid = self.get_appid(installer, initial=appid)
self.variables = installer.get("variables", {})
self.files = [
InstallerFile(self.game_slug, file_id, file_meta)
for file_desc in self.script.get("files", [])
for file_id, file_meta in file_desc.items()
]
self.requires = self.script.get("requires")
self.extends = self.script.get("extends")
self.game_id = self.get_game_id()
self.is_gog = False
def get_service(self, initial=None):
if initial:
return initial
if "steam" in self.runner and "steam" in SERVICES:
return SERVICES["steam"]()
version = self.version.lower()
if "humble" in version and "humblebundle" in SERVICES:
return SERVICES["humblebundle"]()
if "gog" in version and "gog" in SERVICES:
return SERVICES["gog"]()
def get_appid(self, installer, initial=None):
if installer.get("is_dlc"):
return installer.get("dlcid")
if initial:
return initial
if not self.service:
return
if self.service.id == "steam":
return installer.get("steamid")
game_config = self.script.get("game", {})
if self.service.id == "gog":
return game_config.get("gogid") or installer.get("gogid")
if self.service.id == "humblebundle":
return game_config.get("humbleid") or installer.get("humblestoreid")
@property
def script_pretty(self):
"""Return a pretty print of the script"""
return json.dumps(self.script, indent=4)
def get_game_id(self):
"""Return the ID of the game in the local DB if one exists"""
# If the game is in the library and uninstalled, the first installation
# updates it
existing_game = get_game_by_field(self.game_slug, "slug")
if existing_game and (self.extends or not existing_game["installed"]):
return existing_game["id"]
@property
def creates_game_folder(self):
"""Determines if an install script should create a game folder for the game"""
if self.requires or self.extends:
# Game is an extension of an existing game, folder exists
return False
if self.runner == "steam":
# Steam games installs in their steamapps directory
return False
if (
self.files
or self.script.get("game", {}).get("gog")
or self.script.get("game", {}).get("prefix")
):
return True
command_names = [list(c.keys())[0] for c in self.script.get("installer", [])]
if "insert-disc" in command_names:
return True
return False
def get_errors(self):
"""Return potential errors in the script"""
errors = []
if not isinstance(self.script, dict):
errors.append("Script must be a dictionary")
# Return early since the method assumes a dict
return errors
# Check that installers contains all required fields
for field in ("runner", "game_name", "game_slug"):
if not hasattr(self, field) or not getattr(self, field):
errors.append("Missing field '%s'" % field)
# Check that libretro installers have a core specified
if self.runner == "libretro":
if "game" not in self.script or "core" not in self.script["game"]:
errors.append("Missing libretro core in game section")
# Check that Steam games have an AppID
if self.runner == "steam":
if not self.script.get("game", {}).get("appid"):
errors.append("Missing appid for Steam game")
# Check that installers don't contain both 'requires' and 'extends'
if self.script.get("requires") and self.script.get("extends"):
errors.append("Scripts can't have both extends and requires")
return errors
def pop_user_provided_file(self):
"""Return and remove the first user provided file, which is used for game stores"""
for index, file in enumerate(self.files):
if file.url.startswith("N/A"):
self.files.pop(index)
return file.id
def prepare_game_files(self, patch_version=None):
"""Gathers necessary files before iterating through them."""
if not self.files:
logger.info("No files to prepare")
return
if not self.service:
logger.debug("No service to retrieve files from")
return
if self.service.online and not self.service.is_connected():
logger.info("Not authenticated to %s", self.service.id)
return
installer_file_id = self.pop_user_provided_file()
if not installer_file_id:
logger.warning("Could not find a file for this service")
return
logger.info("Getting files for %s", installer_file_id)
if self.service.has_extras:
logger.info("Adding selected extras to downloads")
self.service.selected_extras = self.interpreter.extras
if patch_version:
# If a patch version is given download the patch files instead of the installer
installer_files = self.service.get_patch_files(self, installer_file_id)
else:
installer_files = self.service.get_installer_files(self, installer_file_id)
for installer_file in installer_files:
self.files.append(installer_file)
if not installer_files:
# Failed to get the service game, put back a user provided file
logger.debug("Unable to get files from service. Setting %s to manual.", installer_file_id)
self.files.insert(0, InstallerFile(self.game_slug, installer_file_id, {
"url": "N/A: Provider installer file",
"filename": ""
}))
def _substitute_config(self, script_config):
"""Substitute values such as $GAMEDIR in a config dict."""
config = {}
for key in script_config:
if not isinstance(key, str):
raise ScriptingError(_("Game config key must be a string"), key)
value = script_config[key]
if str(value).lower() == 'true':
value = True
if str(value).lower() == 'false':
value = False
if key == "launch_configs":
# launch configuration don't need substitutions at least for now.
config[key] = value
elif isinstance(value, list):
config[key] = [self.interpreter._substitute(i) for i in value]
elif isinstance(value, dict):
config[key] = {k: self.interpreter._substitute(v) for (k, v) in value.items()}
elif isinstance(value, bool):
config[key] = value
else:
config[key] = self.interpreter._substitute(value)
return config
def get_game_config(self):
"""Return the game configuration"""
if self.requires:
# Load the base game config
required_game = get_game_by_field(self.requires, field="installer_slug")
if not required_game:
required_game = get_game_by_field(self.requires, field="slug")
if not required_game:
raise ValueError("No game matched '%s' on installer_slug or slug" % self.requires)
base_config = LutrisConfig(
runner_slug=self.runner, game_config_id=required_game["configpath"]
)
config = base_config.game_level
else:
config = {"game": {}}
# Config update
if "system" in self.script:
config["system"] = self._substitute_config(self.script["system"])
if self.runner in self.script and self.script[self.runner]:
config[self.runner] = self._substitute_config(self.script[self.runner])
launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)
if launcher:
config["game"][launcher] = launcher_config
if "game" in self.script:
try:
config["game"].update(self.script["game"])
except ValueError as err:
raise ScriptingError(_("Invalid 'game' section"), self.script["game"]) from err
config["game"] = self._substitute_config(config["game"])
if AUTO_ELF_EXE in config["game"].get("exe", ""):
config["game"]["exe"] = find_linux_game_executable(self.interpreter.target_path,
make_executable=True)
elif AUTO_WIN32_EXE in config["game"].get("exe", ""):
config["game"]["exe"] = find_windows_game_executable(self.interpreter.target_path)
return config
def save(self):
"""Write the game configuration in the DB and config file"""
if self.extends:
logger.info(
"This is an extension to %s, not creating a new game entry",
self.extends,
)
return self.game_id
if self.is_gog:
gog_config = get_gog_config_from_path(self.interpreter.target_path)
if gog_config:
gog_game_path = get_gog_game_path(self.interpreter.target_path)
lutris_config = convert_gog_config_to_lutris(gog_config, gog_game_path)
self.script["game"].update(lutris_config)
configpath = write_game_config(self.slug, self.get_game_config())
runner_inst = import_runner(self.runner)()
if self.service:
service_id = self.service.id
else:
service_id = None
self.game_id = add_or_update(
name=self.game_name,
runner=self.runner,
slug=self.game_slug,
platform=runner_inst.get_platform(),
directory=self.interpreter.target_path,
installed=1,
hidden=0,
installer_slug=self.slug,
parent_slug=self.requires,
year=self.year,
configpath=configpath,
service=service_id,
service_id=self.service_appid,
id=self.game_id,
)
return self.game_id
def get_game_launcher_config(self, game_files):
"""Game options such as exe or main_file can be added at the root of the
script as a shortcut, this integrates them into the game config properly
This should be deprecated. Game launchers should go in the game section.
"""
launcher, launcher_value = get_game_launcher(self.script)
if isinstance(launcher_value, list):
launcher_values = []
for game_file in launcher_value:
if game_file in game_files:
launcher_values.append(game_files[game_file])
else:
launcher_values.append(game_file)
return launcher, launcher_values
if launcher_value:
if launcher_value in game_files:
launcher_value = game_files[launcher_value]
elif self.interpreter.target_path and os.path.exists(
os.path.join(self.interpreter.target_path, launcher_value)
):
launcher_value = os.path.join(self.interpreter.target_path, launcher_value)
return launcher, launcher_value
creates_game_folder
property
readonly
¶
Determines if an install script should create a game folder for the game
script_pretty
property
readonly
¶
Return a pretty print of the script
__init__(self, installer, interpreter, service, appid)
special
¶
Source code in lutris/installer/installer.py
def __init__(self, installer, interpreter, service, appid):
self.interpreter = interpreter
self.installer = installer
self.is_update = False
self.version = installer["version"]
self.slug = installer["slug"]
self.year = installer.get("year")
self.runner = installer["runner"]
self.script = installer.get("script")
self.game_name = installer["name"]
self.game_slug = installer["game_slug"]
self.service = self.get_service(initial=service)
self.service_appid = self.get_appid(installer, initial=appid)
self.variables = installer.get("variables", {})
self.files = [
InstallerFile(self.game_slug, file_id, file_meta)
for file_desc in self.script.get("files", [])
for file_id, file_meta in file_desc.items()
]
self.requires = self.script.get("requires")
self.extends = self.script.get("extends")
self.game_id = self.get_game_id()
self.is_gog = False
get_appid(self, installer, initial=None)
¶
Source code in lutris/installer/installer.py
def get_appid(self, installer, initial=None):
if installer.get("is_dlc"):
return installer.get("dlcid")
if initial:
return initial
if not self.service:
return
if self.service.id == "steam":
return installer.get("steamid")
game_config = self.script.get("game", {})
if self.service.id == "gog":
return game_config.get("gogid") or installer.get("gogid")
if self.service.id == "humblebundle":
return game_config.get("humbleid") or installer.get("humblestoreid")
get_errors(self)
¶
Return potential errors in the script
Source code in lutris/installer/installer.py
def get_errors(self):
"""Return potential errors in the script"""
errors = []
if not isinstance(self.script, dict):
errors.append("Script must be a dictionary")
# Return early since the method assumes a dict
return errors
# Check that installers contains all required fields
for field in ("runner", "game_name", "game_slug"):
if not hasattr(self, field) or not getattr(self, field):
errors.append("Missing field '%s'" % field)
# Check that libretro installers have a core specified
if self.runner == "libretro":
if "game" not in self.script or "core" not in self.script["game"]:
errors.append("Missing libretro core in game section")
# Check that Steam games have an AppID
if self.runner == "steam":
if not self.script.get("game", {}).get("appid"):
errors.append("Missing appid for Steam game")
# Check that installers don't contain both 'requires' and 'extends'
if self.script.get("requires") and self.script.get("extends"):
errors.append("Scripts can't have both extends and requires")
return errors
get_game_config(self)
¶
Return the game configuration
Source code in lutris/installer/installer.py
def get_game_config(self):
"""Return the game configuration"""
if self.requires:
# Load the base game config
required_game = get_game_by_field(self.requires, field="installer_slug")
if not required_game:
required_game = get_game_by_field(self.requires, field="slug")
if not required_game:
raise ValueError("No game matched '%s' on installer_slug or slug" % self.requires)
base_config = LutrisConfig(
runner_slug=self.runner, game_config_id=required_game["configpath"]
)
config = base_config.game_level
else:
config = {"game": {}}
# Config update
if "system" in self.script:
config["system"] = self._substitute_config(self.script["system"])
if self.runner in self.script and self.script[self.runner]:
config[self.runner] = self._substitute_config(self.script[self.runner])
launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)
if launcher:
config["game"][launcher] = launcher_config
if "game" in self.script:
try:
config["game"].update(self.script["game"])
except ValueError as err:
raise ScriptingError(_("Invalid 'game' section"), self.script["game"]) from err
config["game"] = self._substitute_config(config["game"])
if AUTO_ELF_EXE in config["game"].get("exe", ""):
config["game"]["exe"] = find_linux_game_executable(self.interpreter.target_path,
make_executable=True)
elif AUTO_WIN32_EXE in config["game"].get("exe", ""):
config["game"]["exe"] = find_windows_game_executable(self.interpreter.target_path)
return config
get_game_id(self)
¶
Return the ID of the game in the local DB if one exists
Source code in lutris/installer/installer.py
def get_game_id(self):
"""Return the ID of the game in the local DB if one exists"""
# If the game is in the library and uninstalled, the first installation
# updates it
existing_game = get_game_by_field(self.game_slug, "slug")
if existing_game and (self.extends or not existing_game["installed"]):
return existing_game["id"]
get_game_launcher_config(self, game_files)
¶
Game options such as exe or main_file can be added at the root of the script as a shortcut, this integrates them into the game config properly This should be deprecated. Game launchers should go in the game section.
Source code in lutris/installer/installer.py
def get_game_launcher_config(self, game_files):
"""Game options such as exe or main_file can be added at the root of the
script as a shortcut, this integrates them into the game config properly
This should be deprecated. Game launchers should go in the game section.
"""
launcher, launcher_value = get_game_launcher(self.script)
if isinstance(launcher_value, list):
launcher_values = []
for game_file in launcher_value:
if game_file in game_files:
launcher_values.append(game_files[game_file])
else:
launcher_values.append(game_file)
return launcher, launcher_values
if launcher_value:
if launcher_value in game_files:
launcher_value = game_files[launcher_value]
elif self.interpreter.target_path and os.path.exists(
os.path.join(self.interpreter.target_path, launcher_value)
):
launcher_value = os.path.join(self.interpreter.target_path, launcher_value)
return launcher, launcher_value
get_service(self, initial=None)
¶
Source code in lutris/installer/installer.py
def get_service(self, initial=None):
if initial:
return initial
if "steam" in self.runner and "steam" in SERVICES:
return SERVICES["steam"]()
version = self.version.lower()
if "humble" in version and "humblebundle" in SERVICES:
return SERVICES["humblebundle"]()
if "gog" in version and "gog" in SERVICES:
return SERVICES["gog"]()
pop_user_provided_file(self)
¶
Return and remove the first user provided file, which is used for game stores
Source code in lutris/installer/installer.py
def pop_user_provided_file(self):
"""Return and remove the first user provided file, which is used for game stores"""
for index, file in enumerate(self.files):
if file.url.startswith("N/A"):
self.files.pop(index)
return file.id
prepare_game_files(self, patch_version=None)
¶
Gathers necessary files before iterating through them.
Source code in lutris/installer/installer.py
def prepare_game_files(self, patch_version=None):
"""Gathers necessary files before iterating through them."""
if not self.files:
logger.info("No files to prepare")
return
if not self.service:
logger.debug("No service to retrieve files from")
return
if self.service.online and not self.service.is_connected():
logger.info("Not authenticated to %s", self.service.id)
return
installer_file_id = self.pop_user_provided_file()
if not installer_file_id:
logger.warning("Could not find a file for this service")
return
logger.info("Getting files for %s", installer_file_id)
if self.service.has_extras:
logger.info("Adding selected extras to downloads")
self.service.selected_extras = self.interpreter.extras
if patch_version:
# If a patch version is given download the patch files instead of the installer
installer_files = self.service.get_patch_files(self, installer_file_id)
else:
installer_files = self.service.get_installer_files(self, installer_file_id)
for installer_file in installer_files:
self.files.append(installer_file)
if not installer_files:
# Failed to get the service game, put back a user provided file
logger.debug("Unable to get files from service. Setting %s to manual.", installer_file_id)
self.files.insert(0, InstallerFile(self.game_slug, installer_file_id, {
"url": "N/A: Provider installer file",
"filename": ""
}))
save(self)
¶
Write the game configuration in the DB and config file
Source code in lutris/installer/installer.py
def save(self):
"""Write the game configuration in the DB and config file"""
if self.extends:
logger.info(
"This is an extension to %s, not creating a new game entry",
self.extends,
)
return self.game_id
if self.is_gog:
gog_config = get_gog_config_from_path(self.interpreter.target_path)
if gog_config:
gog_game_path = get_gog_game_path(self.interpreter.target_path)
lutris_config = convert_gog_config_to_lutris(gog_config, gog_game_path)
self.script["game"].update(lutris_config)
configpath = write_game_config(self.slug, self.get_game_config())
runner_inst = import_runner(self.runner)()
if self.service:
service_id = self.service.id
else:
service_id = None
self.game_id = add_or_update(
name=self.game_name,
runner=self.runner,
slug=self.game_slug,
platform=runner_inst.get_platform(),
directory=self.interpreter.target_path,
installed=1,
hidden=0,
installer_slug=self.slug,
parent_slug=self.requires,
year=self.year,
configpath=configpath,
service=service_id,
service_id=self.service_appid,
id=self.game_id,
)
return self.game_id
installer_file
¶
Manipulates installer files
InstallerFile
¶
Representation of a file in the files sections of an installer
Source code in lutris/installer/installer_file.py
class InstallerFile:
"""Representation of a file in the `files` sections of an installer"""
def __init__(self, game_slug, file_id, file_meta):
self.game_slug = game_slug
self.id = file_id.replace("-", "_") # pylint: disable=invalid-name
self._file_meta = file_meta
self._dest_file = None # Used to override the destination
@property
def url(self):
_url = ""
if isinstance(self._file_meta, dict):
if "url" not in self._file_meta:
raise ScriptingError(_("missing field `url` for file `%s`") % self.id)
_url = self._file_meta["url"]
else:
_url = self._file_meta
if _url.startswith("/"):
return "file://" + _url
return _url
@property
def filename(self):
if isinstance(self._file_meta, dict):
if "filename" not in self._file_meta:
raise ScriptingError(_("missing field `filename` in file `%s`") % self.id)
return self._file_meta["filename"]
if self._file_meta.startswith("N/A"):
if self.uses_pga_cache() and os.path.isdir(self.cache_path):
return self.cached_filename
return ""
if self.url.startswith("$STEAM"):
return self.url
return os.path.basename(self._file_meta)
@property
def referer(self):
if isinstance(self._file_meta, dict):
return self._file_meta.get("referer")
@property
def checksum(self):
if isinstance(self._file_meta, dict):
return self._file_meta.get("checksum")
@property
def dest_file(self):
if self._dest_file:
return self._dest_file
return os.path.join(self.cache_path, self.filename)
@dest_file.setter
def dest_file(self, value):
self._dest_file = value
def __str__(self):
return "%s/%s" % (self.game_slug, self.id)
@property
def human_url(self):
"""Return the url in human readable format"""
if self.url.startswith("N/A"):
# Ask the user where the file is located
parts = self.url.split(":", 1)
if len(parts) == 2:
return parts[1]
return "Please select file '%s'" % self.id
return self.url
@property
def cached_filename(self):
"""Return the filename of the first file in the cache path"""
cache_files = os.listdir(self.cache_path)
if cache_files:
return cache_files[0]
return ""
@property
def provider(self):
"""Return file provider used"""
if self.url.startswith("$STEAM"):
return "steam"
if self.is_cached:
return "pga"
if self.url.startswith("N/A"):
return "user"
if self.is_downloadable():
return "download"
raise ValueError("Unsupported provider for %s" % self.url)
@property
def providers(self):
"""Return all supported providers"""
_providers = set()
if self.url.startswith("$STEAM"):
_providers.add("steam")
if self.is_cached:
_providers.add("pga")
if self.url.startswith("N/A"):
_providers.add("user")
if self.is_downloadable():
_providers.add("download")
return _providers
def is_downloadable(self):
"""Return True if the file can be downloaded (even from the local filesystem)"""
return self.url.startswith(("http", "file"))
def uses_pga_cache(self, create=False):
"""Determines whether the installer files are stored in a PGA cache
Params:
create (bool): If a cache is active, auto create directories if needed
Returns:
bool
"""
cache_path = cache.get_cache_path()
if not cache_path:
return False
if system.path_exists(cache_path):
return True
if create:
try:
logger.debug("Creating cache path %s", self.cache_path)
os.makedirs(self.cache_path)
except (OSError, PermissionError) as ex:
logger.error("Failed to created cache path: %s", ex)
return False
return True
logger.warning("Cache path %s does not exist", cache_path)
return False
@property
def cache_path(self):
"""Return the directory used as a cache for the duration of the installation"""
_cache_path = cache.get_cache_path()
if not _cache_path:
_cache_path = os.path.join(settings.CACHE_DIR, "installer")
url_parts = urlparse(self.url)
if url_parts.netloc.endswith("gog.com"):
folder = "gog"
else:
folder = self.id
return os.path.join(_cache_path, self.game_slug, folder)
def prepare(self):
"""Prepare the file for download"""
if not system.path_exists(self.cache_path):
os.makedirs(self.cache_path)
def check_hash(self):
"""Checks the checksum of `file` and compare it to `value`
Args:
checksum (str): The checksum to look for (type:hash)
dest_file (str): The path to the destination file
dest_file_uri (str): The uri for the destination file
"""
if not self.checksum or not self.dest_file:
return
try:
hash_type, expected_hash = self.checksum.split(':', 1)
except ValueError as err:
raise ScriptingError(_("Invalid checksum, expected format (type:hash) "), self.checksum) from err
if system.get_file_checksum(self.dest_file, hash_type) != expected_hash:
raise ScriptingError(hash_type.capitalize() + _(" checksum mismatch "), self.checksum)
@property
def is_cached(self):
"""Is the file available in the local PGA cache?"""
return self.uses_pga_cache() and system.path_exists(self.dest_file)
cache_path
property
readonly
¶
Return the directory used as a cache for the duration of the installation
cached_filename
property
readonly
¶
Return the filename of the first file in the cache path
checksum
property
readonly
¶
dest_file
property
writable
¶
filename
property
readonly
¶
human_url
property
readonly
¶
Return the url in human readable format
is_cached
property
readonly
¶
Is the file available in the local PGA cache?
provider
property
readonly
¶
Return file provider used
providers
property
readonly
¶
Return all supported providers
referer
property
readonly
¶
url
property
readonly
¶
__init__(self, game_slug, file_id, file_meta)
special
¶
Source code in lutris/installer/installer_file.py
def __init__(self, game_slug, file_id, file_meta):
self.game_slug = game_slug
self.id = file_id.replace("-", "_") # pylint: disable=invalid-name
self._file_meta = file_meta
self._dest_file = None # Used to override the destination
__str__(self)
special
¶
Source code in lutris/installer/installer_file.py
def __str__(self):
return "%s/%s" % (self.game_slug, self.id)
check_hash(self)
¶
Checks the checksum of file and compare it to value
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checksum |
str |
The checksum to look for (type:hash) |
required |
dest_file |
str |
The path to the destination file |
required |
dest_file_uri |
str |
The uri for the destination file |
required |
Source code in lutris/installer/installer_file.py
def check_hash(self):
"""Checks the checksum of `file` and compare it to `value`
Args:
checksum (str): The checksum to look for (type:hash)
dest_file (str): The path to the destination file
dest_file_uri (str): The uri for the destination file
"""
if not self.checksum or not self.dest_file:
return
try:
hash_type, expected_hash = self.checksum.split(':', 1)
except ValueError as err:
raise ScriptingError(_("Invalid checksum, expected format (type:hash) "), self.checksum) from err
if system.get_file_checksum(self.dest_file, hash_type) != expected_hash:
raise ScriptingError(hash_type.capitalize() + _(" checksum mismatch "), self.checksum)
is_downloadable(self)
¶
Return True if the file can be downloaded (even from the local filesystem)
Source code in lutris/installer/installer_file.py
def is_downloadable(self):
"""Return True if the file can be downloaded (even from the local filesystem)"""
return self.url.startswith(("http", "file"))
prepare(self)
¶
Prepare the file for download
Source code in lutris/installer/installer_file.py
def prepare(self):
"""Prepare the file for download"""
if not system.path_exists(self.cache_path):
os.makedirs(self.cache_path)
uses_pga_cache(self, create=False)
¶
Determines whether the installer files are stored in a PGA cache
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
create |
bool |
If a cache is active, auto create directories if needed |
False |
Returns:
| Type | Description |
|---|---|
bool |
Source code in lutris/installer/installer_file.py
def uses_pga_cache(self, create=False):
"""Determines whether the installer files are stored in a PGA cache
Params:
create (bool): If a cache is active, auto create directories if needed
Returns:
bool
"""
cache_path = cache.get_cache_path()
if not cache_path:
return False
if system.path_exists(cache_path):
return True
if create:
try:
logger.debug("Creating cache path %s", self.cache_path)
os.makedirs(self.cache_path)
except (OSError, PermissionError) as ex:
logger.error("Failed to created cache path: %s", ex)
return False
return True
logger.warning("Cache path %s does not exist", cache_path)
return False
interpreter
¶
Install a game by following its install script.
ScriptInterpreter (Object, CommandsMixin)
¶
Control the execution of an installer
Source code in lutris/installer/interpreter.py
class ScriptInterpreter(GObject.Object, CommandsMixin):
"""Control the execution of an installer"""
__gsignals__ = {
"runners-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, installer, parent=None):
super().__init__()
self.target_path = None
self.parent = parent
self.service = parent.service if parent else None
_appid = parent.appid if parent else None
self.game_dir_created = False # Whether a game folder was created during the install
# Extra files for installers, either None if the extras haven't been checked yet.
# Or a list of IDs of extras to be downloaded during the install
self.extras = None
self.game_disc = None
self.game_files = {}
self.cancelled = False
self.abort_current_task = None
self.user_inputs = []
self.current_command = 0 # Current installer command when iterating through them
self.runners_to_install = []
self.installer = LutrisInstaller(installer, self, service=self.service, appid=_appid)
if not self.installer.script:
raise ScriptingError(_("This installer doesn't have a 'script' section"))
script_errors = self.installer.get_errors()
if script_errors:
raise ScriptingError(
_("Invalid script: \n{}").format("\n".join(script_errors)), self.installer.script
)
self.current_resolution = DISPLAY_MANAGER.get_current_resolution()
self._check_binary_dependencies()
self._check_dependency()
if self.installer.creates_game_folder:
self.target_path = self.get_default_target()
@property
def appid(self):
logger.warning("Do not access appid from interpreter")
return self.installer.service_appid
def get_default_target(self):
"""Return default installation dir"""
config = LutrisConfig(runner_slug=self.installer.runner)
games_dir = config.system_config.get("game_path", os.path.expanduser("~"))
if self.service:
service_dir = self.service.id
else:
service_dir = ""
return os.path.expanduser(os.path.join(games_dir, service_dir, self.installer.game_slug))
@property
def cache_path(self):
"""Return the directory used as a cache for the duration of the installation"""
return os.path.join(settings.CACHE_DIR, "installer/%s" % self.installer.game_slug)
@property
def script_env(self):
"""Return the script's own environment variable with values
susbtituted. This value can be used to provide the same environment
variable as set for the game during the install process.
"""
return {
key: self._substitute(value) for key, value in
self.installer.script.get('system', {}).get('env', {}).items()
}
@staticmethod
def _get_game_dependency(dependency):
"""Return a game database row from a dependency name"""
game = get_game_by_field(dependency, field="installer_slug")
if not game:
game = get_game_by_field(dependency, "slug")
# Game must be installed and have a directory
# set so we can use that as the destination
if game and game["installed"] and game["directory"]:
return game
def _check_binary_dependencies(self):
"""Check if all required binaries are installed on the system.
This reads a `require-binaries` entry in the script, parsed the same way as
the `requires` entry.
"""
binary_dependencies = unpack_dependencies(self.installer.script.get("require-binaries"))
for dependency in binary_dependencies:
if isinstance(dependency, tuple):
installed_binaries = {
dependency_option: bool(system.find_executable(dependency_option))
for dependency_option in dependency
}
if not any(installed_binaries.values()):
raise ScriptingError(_("This installer requires %s on your system") % _(" or ").join(dependency))
else:
if not system.find_executable(dependency):
raise ScriptingError(_("This installer requires %s on your system") % dependency)
def _check_dependency(self):
"""When a game is a mod or an extension of another game, check that the base
game is installed.
If the game is available, install the game in the base game folder.
The first game available listed in the dependencies is the one picked to base
the installed on.
"""
if self.installer.extends:
dependencies = [self.installer.extends]
else:
dependencies = unpack_dependencies(self.installer.requires)
error_message = _("You need to install {} before")
for index, dependency in enumerate(dependencies):
if isinstance(dependency, tuple):
installed_games = [dep for dep in [self._get_game_dependency(dep) for dep in dependency] if dep]
if not installed_games:
if len(dependency) == 1:
raise MissingGameDependency(slug=dependency)
raise ScriptingError(error_message.format(_(" or ").join(dependency)))
if index == 0:
self.target_path = installed_games[0]["directory"]
self.requires = installed_games[0]["installer_slug"]
else:
game = self._get_game_dependency(dependency)
if not game:
raise MissingGameDependency(slug=dependency)
if index == 0:
self.target_path = game["directory"]
self.requires = game["installer_slug"]
def get_extras(self):
"""Get extras and store them to move them at the end of the install"""
logger.debug("Checking if service provide extra files")
if not self.service or not self.service.has_extras:
self.extras = []
return self.extras
self.extras = self.service.get_extras(self.installer.service_appid)
return self.extras
def launch_install(self):
"""Launch the install process"""
self.runners_to_install = self.get_runners_to_install()
self.install_runners()
self.create_game_folder()
def create_game_folder(self):
"""Create the game folder if needed and store if is was created"""
if (
self.installer.files
and self.target_path
and not system.path_exists(self.target_path)
and self.installer.creates_game_folder
):
try:
logger.debug("Creating destination path %s", self.target_path)
os.makedirs(self.target_path)
self.game_dir_created = True
except PermissionError as err:
raise ScriptingError(
_("Lutris does not have the necessary permissions to install to path:"),
self.target_path,
) from err
def get_runners_to_install(self):
"""Check if the runner is installed before starting the installation
Install the required runner(s) if necessary. This should handle runner
dependencies or runners used for installer tasks.
"""
runners_to_install = []
required_runners = []
runner = self.get_runner_class(self.installer.runner)
required_runners.append(runner())
for command in self.installer.script.get("installer", []):
command_name, command_params = self._get_command_name_and_params(command)
if command_name == "task":
runner_name, _task_name = self._get_task_runner_and_name(command_params["name"])
runner_names = [r.name for r in required_runners]
if runner_name not in runner_names:
required_runners.append(self.get_runner_class(runner_name)())
for runner in required_runners:
params = {}
if self.installer.runner == "libretro":
params["core"] = self.installer.script["game"]["core"]
if self.installer.runner.startswith("wine"):
# Force the wine version to be installed
params["fallback"] = False
params["min_version"] = wine.MIN_SAFE_VERSION
version = self._get_runner_version()
if version:
params["version"] = version
else:
# Looking up default wine version
default_wine = runner.get_runner_version() or {}
if "version" in default_wine:
logger.debug("Default wine version is %s", default_wine["version"])
# Set the version to both the is_installed params and
# the script itself so the version gets saved at the
# end of the install.
if self.installer.runner not in self.installer.script:
self.installer.script[self.installer.runner] = {}
version = "{}-{}".format(default_wine["version"],
default_wine["architecture"])
params["version"] = \
self.installer.script[self.installer.runner]["version"] = version
else:
logger.error("Failed to get default wine version (got %s)", default_wine)
if not runner.is_installed(**params):
logger.info("Runner %s needs to be installed", runner)
runners_to_install.append(runner)
if self.installer.runner.startswith("wine") and not get_wine_version():
WineNotInstalledWarning(parent=self.parent)
return runners_to_install
def install_runners(self):
"""Install required runners for a game"""
if self.runners_to_install:
self.install_runner(self.runners_to_install.pop(0))
return
self.emit("runners-installed")
def install_runner(self, runner):
"""Install runner required by the install script"""
logger.debug("Installing %s", runner.name)
try:
runner.install(
version=self._get_runner_version(),
downloader=simple_downloader,
callback=self.install_runners,
)
except (NonInstallableRunnerError, RunnerInstallationError) as ex:
logger.error(ex.message)
raise ScriptingError(ex.message) from ex
def get_runner_class(self, runner_name):
"""Runner the runner class from its name"""
try:
runner = import_runner(runner_name)
except InvalidRunner as err:
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
raise ScriptingError(_("Invalid runner provided %s") % runner_name) from err
return runner
def launch_installer_commands(self):
"""Run the pre-installation steps and launch install."""
if self.target_path and os.path.exists(self.target_path):
os.chdir(self.target_path)
os.makedirs(self.cache_path, exist_ok=True)
# Copy extras to game folder
if len(self.extras) == len(self.installer.files):
# Reset the install script in case there are only extras.
logger.warning("Installer with only extras and no game files")
self.installer.script["installer"] = []
for extra in self.extras:
self.installer.script["installer"].append(
{"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
)
self._iter_commands()
def _iter_commands(self, result=None, exception=None):
if result == "STOP" or self.cancelled:
return
self.parent.set_status(_("Installing game data"))
self.parent.add_spinner()
self.parent.continue_button.hide()
commands = self.installer.script.get("installer", [])
if exception:
logger.error("Last install command failed, show error")
self.parent.on_install_error(repr(exception))
elif self.current_command < len(commands):
try:
command = commands[self.current_command]
except KeyError as err:
raise ScriptingError(_("Installer commands are not formatted correctly")) from err
self.current_command += 1
method, params = self._map_command(command)
if isinstance(params, dict):
status_text = params.pop("description", None)
else:
status_text = None
if status_text:
self.parent.set_status(status_text)
logger.debug("Installer command: %s", command)
AsyncCall(method, self._iter_commands, params)
else:
self._finish_install()
@staticmethod
def _get_command_name_and_params(command_data):
if isinstance(command_data, dict):
command_name = list(command_data.keys())[0]
command_params = command_data[command_name]
else:
command_name = command_data
command_params = {}
command_name = command_name.replace("-", "_")
# Prevent private methods from being accessed as commands
command_name = command_name.strip("_")
return command_name, command_params
def _map_command(self, command_data):
"""Map a directive from the `installer` section to an internal
method."""
command_name, command_params = self._get_command_name_and_params(command_data)
if not hasattr(self, command_name):
raise ScriptingError(_('The command "%s" does not exist.') % command_name)
return getattr(self, command_name), command_params
def _finish_install(self):
game_id = self.installer.save()
launcher_value = None
if self.installer.script.get("game"):
_launcher, launcher_value = get_game_launcher(self.installer.script)
path = None
if launcher_value:
path = self._substitute(launcher_value)
if not os.path.isabs(path) and self.target_path:
path = os.path.join(self.target_path, path)
if path and not os.path.isfile(path) and self.installer.runner not in ("web", "browser"):
self.parent.set_status(
_(
"The executable at path %s can't be found, please check the destination folder.\n"
"Some parts of the installation process may have not completed successfully."
) % path
)
logger.warning("No executable found at specified location %s", path)
else:
install_complete_text = (self.installer.script.get("install_complete_text") or _("Installation completed!"))
self.parent.set_status(install_complete_text)
download_lutris_media(self.installer.game_slug)
self.parent.on_install_finished(game_id)
def cleanup(self):
"""Clean up install dir after a successful install"""
os.chdir(os.path.expanduser("~"))
system.remove_folder(self.cache_path)
def revert(self, remove_game_dir=True):
"""Revert installation in case of an error"""
logger.info("Cancelling installation of %s", self.installer.game_name)
if self.installer.runner.startswith("wine"):
self.task({"name": "winekill"})
self.cancelled = True
if self.abort_current_task:
self.abort_current_task()
if self.target_path and remove_game_dir:
system.remove_folder(self.target_path)
def _get_string_replacements(self):
"""Return a mapping of variables to their actual value"""
replacements = {
"GAMEDIR": self.target_path,
"CACHE": self.cache_path,
"HOME": os.path.expanduser("~"),
"STEAM_DATA_DIR": steam.steam().steam_data_dir,
"DISC": self.game_disc,
"USER": os.getenv("USER"),
"INPUT": self.user_inputs[-1]["value"] if self.user_inputs else "",
"VERSION": self.installer.version,
"RESOLUTION": "x".join(self.current_resolution),
"RESOLUTION_WIDTH": self.current_resolution[0],
"RESOLUTION_HEIGHT": self.current_resolution[1],
"RESOLUTION_WIDTH_HEX": hex(int(self.current_resolution[0])),
"RESOLUTION_HEIGHT_HEX": hex(int(self.current_resolution[1])),
"WINEBIN": self.get_wine_path(),
}
replacements.update(self.installer.variables)
# Add 'INPUT_<id>' replacements for user inputs with an id
for input_data in self.user_inputs:
alias = input_data["alias"]
if alias:
replacements[alias] = input_data["value"]
replacements.update(self.game_files)
return replacements
def _substitute(self, template_string):
"""Replace path aliases with real paths."""
if template_string is None:
logger.warning("No template string given")
return ""
if str(template_string).replace("-", "_") in self.game_files:
template_string = template_string.replace("-", "_")
return system.substitute(template_string, self._get_string_replacements())
def eject_wine_disc(self):
"""Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes"""
wine_path = get_wine_version_exe(self._get_runner_version())
wine.eject_disc(wine_path, self.target_path)
appid
property
readonly
¶
cache_path
property
readonly
¶
Return the directory used as a cache for the duration of the installation
script_env
property
readonly
¶
Return the script's own environment variable with values susbtituted. This value can be used to provide the same environment variable as set for the game during the install process.
__init__(self, installer, parent=None)
special
¶
Source code in lutris/installer/interpreter.py
def __init__(self, installer, parent=None):
super().__init__()
self.target_path = None
self.parent = parent
self.service = parent.service if parent else None
_appid = parent.appid if parent else None
self.game_dir_created = False # Whether a game folder was created during the install
# Extra files for installers, either None if the extras haven't been checked yet.
# Or a list of IDs of extras to be downloaded during the install
self.extras = None
self.game_disc = None
self.game_files = {}
self.cancelled = False
self.abort_current_task = None
self.user_inputs = []
self.current_command = 0 # Current installer command when iterating through them
self.runners_to_install = []
self.installer = LutrisInstaller(installer, self, service=self.service, appid=_appid)
if not self.installer.script:
raise ScriptingError(_("This installer doesn't have a 'script' section"))
script_errors = self.installer.get_errors()
if script_errors:
raise ScriptingError(
_("Invalid script: \n{}").format("\n".join(script_errors)), self.installer.script
)
self.current_resolution = DISPLAY_MANAGER.get_current_resolution()
self._check_binary_dependencies()
self._check_dependency()
if self.installer.creates_game_folder:
self.target_path = self.get_default_target()
cleanup(self)
¶
Clean up install dir after a successful install
Source code in lutris/installer/interpreter.py
def cleanup(self):
"""Clean up install dir after a successful install"""
os.chdir(os.path.expanduser("~"))
system.remove_folder(self.cache_path)
create_game_folder(self)
¶
Create the game folder if needed and store if is was created
Source code in lutris/installer/interpreter.py
def create_game_folder(self):
"""Create the game folder if needed and store if is was created"""
if (
self.installer.files
and self.target_path
and not system.path_exists(self.target_path)
and self.installer.creates_game_folder
):
try:
logger.debug("Creating destination path %s", self.target_path)
os.makedirs(self.target_path)
self.game_dir_created = True
except PermissionError as err:
raise ScriptingError(
_("Lutris does not have the necessary permissions to install to path:"),
self.target_path,
) from err
eject_wine_disc(self)
¶
Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes
Source code in lutris/installer/interpreter.py
def eject_wine_disc(self):
"""Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes"""
wine_path = get_wine_version_exe(self._get_runner_version())
wine.eject_disc(wine_path, self.target_path)
get_default_target(self)
¶
Return default installation dir
Source code in lutris/installer/interpreter.py
def get_default_target(self):
"""Return default installation dir"""
config = LutrisConfig(runner_slug=self.installer.runner)
games_dir = config.system_config.get("game_path", os.path.expanduser("~"))
if self.service:
service_dir = self.service.id
else:
service_dir = ""
return os.path.expanduser(os.path.join(games_dir, service_dir, self.installer.game_slug))
get_extras(self)
¶
Get extras and store them to move them at the end of the install
Source code in lutris/installer/interpreter.py
def get_extras(self):
"""Get extras and store them to move them at the end of the install"""
logger.debug("Checking if service provide extra files")
if not self.service or not self.service.has_extras:
self.extras = []
return self.extras
self.extras = self.service.get_extras(self.installer.service_appid)
return self.extras
get_runner_class(self, runner_name)
¶
Runner the runner class from its name
Source code in lutris/installer/interpreter.py
def get_runner_class(self, runner_name):
"""Runner the runner class from its name"""
try:
runner = import_runner(runner_name)
except InvalidRunner as err:
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
raise ScriptingError(_("Invalid runner provided %s") % runner_name) from err
return runner
get_runners_to_install(self)
¶
Check if the runner is installed before starting the installation Install the required runner(s) if necessary. This should handle runner dependencies or runners used for installer tasks.
Source code in lutris/installer/interpreter.py
def get_runners_to_install(self):
"""Check if the runner is installed before starting the installation
Install the required runner(s) if necessary. This should handle runner
dependencies or runners used for installer tasks.
"""
runners_to_install = []
required_runners = []
runner = self.get_runner_class(self.installer.runner)
required_runners.append(runner())
for command in self.installer.script.get("installer", []):
command_name, command_params = self._get_command_name_and_params(command)
if command_name == "task":
runner_name, _task_name = self._get_task_runner_and_name(command_params["name"])
runner_names = [r.name for r in required_runners]
if runner_name not in runner_names:
required_runners.append(self.get_runner_class(runner_name)())
for runner in required_runners:
params = {}
if self.installer.runner == "libretro":
params["core"] = self.installer.script["game"]["core"]
if self.installer.runner.startswith("wine"):
# Force the wine version to be installed
params["fallback"] = False
params["min_version"] = wine.MIN_SAFE_VERSION
version = self._get_runner_version()
if version:
params["version"] = version
else:
# Looking up default wine version
default_wine = runner.get_runner_version() or {}
if "version" in default_wine:
logger.debug("Default wine version is %s", default_wine["version"])
# Set the version to both the is_installed params and
# the script itself so the version gets saved at the
# end of the install.
if self.installer.runner not in self.installer.script:
self.installer.script[self.installer.runner] = {}
version = "{}-{}".format(default_wine["version"],
default_wine["architecture"])
params["version"] = \
self.installer.script[self.installer.runner]["version"] = version
else:
logger.error("Failed to get default wine version (got %s)", default_wine)
if not runner.is_installed(**params):
logger.info("Runner %s needs to be installed", runner)
runners_to_install.append(runner)
if self.installer.runner.startswith("wine") and not get_wine_version():
WineNotInstalledWarning(parent=self.parent)
return runners_to_install
install_runner(self, runner)
¶
Install runner required by the install script
Source code in lutris/installer/interpreter.py
def install_runner(self, runner):
"""Install runner required by the install script"""
logger.debug("Installing %s", runner.name)
try:
runner.install(
version=self._get_runner_version(),
downloader=simple_downloader,
callback=self.install_runners,
)
except (NonInstallableRunnerError, RunnerInstallationError) as ex:
logger.error(ex.message)
raise ScriptingError(ex.message) from ex
install_runners(self)
¶
Install required runners for a game
Source code in lutris/installer/interpreter.py
def install_runners(self):
"""Install required runners for a game"""
if self.runners_to_install:
self.install_runner(self.runners_to_install.pop(0))
return
self.emit("runners-installed")
launch_install(self)
¶
Launch the install process
Source code in lutris/installer/interpreter.py
def launch_install(self):
"""Launch the install process"""
self.runners_to_install = self.get_runners_to_install()
self.install_runners()
self.create_game_folder()
launch_installer_commands(self)
¶
Run the pre-installation steps and launch install.
Source code in lutris/installer/interpreter.py
def launch_installer_commands(self):
"""Run the pre-installation steps and launch install."""
if self.target_path and os.path.exists(self.target_path):
os.chdir(self.target_path)
os.makedirs(self.cache_path, exist_ok=True)
# Copy extras to game folder
if len(self.extras) == len(self.installer.files):
# Reset the install script in case there are only extras.
logger.warning("Installer with only extras and no game files")
self.installer.script["installer"] = []
for extra in self.extras:
self.installer.script["installer"].append(
{"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
)
self._iter_commands()
revert(self, remove_game_dir=True)
¶
Revert installation in case of an error
Source code in lutris/installer/interpreter.py
def revert(self, remove_game_dir=True):
"""Revert installation in case of an error"""
logger.info("Cancelling installation of %s", self.installer.game_name)
if self.installer.runner.startswith("wine"):
self.task({"name": "winekill"})
self.cancelled = True
if self.abort_current_task:
self.abort_current_task()
if self.target_path and remove_game_dir:
system.remove_folder(self.target_path)
legacy
¶
get_game_launcher(script)
¶
Return the key and value of the launcher exe64 can be provided to specify an executable for 64bit systems This should be deprecated when support for multiple binaries has been added.
Source code in lutris/installer/legacy.py
def get_game_launcher(script):
"""Return the key and value of the launcher
exe64 can be provided to specify an executable for 64bit systems
This should be deprecated when support for multiple binaries has been
added.
"""
launcher_value = None
exe = "exe64" if "exe64" in script and linux.LINUX_SYSTEM.is_64_bit else "exe"
for launcher in (exe, "iso", "rom", "disk", "main_file"):
if launcher not in script:
continue
launcher_value = script[launcher]
if launcher == "exe64":
launcher = "exe" # If exe64 is used, rename it to exe
break
if not launcher_value:
launcher = None
return launcher, launcher_value
steam_installer
¶
Collection of installer files
SteamInstaller (Object)
¶
Handles installation of Steam games
Source code in lutris/installer/steam_installer.py
class SteamInstaller(GObject.Object):
"""Handles installation of Steam games"""
__gsignals__ = {
"steam-game-installed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
"steam-state-changed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
}
def __init__(self, steam_uri, file_id):
"""
Params:
steam_uri: Colon separated game info containing:
- $STEAM
- The Steam appid
- The relative path of files to retrieve
file_id: The lutris installer internal id for the game files
"""
super().__init__()
self.steam_poll = None
self.prev_states = [] # Previous states for the Steam installer
self.state = ""
self.install_start_time = None
self.steam_uri = steam_uri
self.stop_func = None
self.cancelled = False
self._runner = None
self.file_id = file_id
try:
_steam, appid, path = self.steam_uri.split(":", 2)
except ValueError as err:
raise ScriptingError(_("Malformed steam path: %s") % self.steam_uri) from err
self.appid = appid
self.path = path
self.platform = "linux"
self.runner = steam.steam()
@property
def steam_rel_path(self):
"""Return the relative path for data files"""
_steam_rel_path = self.path.strip()
if _steam_rel_path == "/":
_steam_rel_path = "."
return _steam_rel_path
@staticmethod
def on_steam_game_installed(_data, error):
"""Callback for Steam game installer, mostly for error handling
since install progress is handled by _monitor_steam_game_install
"""
if error:
raise ScriptingError(str(error))
def install_steam_game(self):
"""Launch installation of a steam game"""
if self.runner.get_game_path_from_appid(appid=self.appid):
logger.info("Steam game %s is already installed", self.appid)
self.emit("steam-game-installed", self.appid)
else:
logger.debug("Installing steam game %s", self.appid)
self.runner.config = LutrisConfig(runner_slug=self.runner.name)
AsyncCall(self.runner.install_game, self.on_steam_game_installed, self.appid)
self.install_start_time = time.localtime()
self.steam_poll = GLib.timeout_add(2000, self._monitor_steam_game_install)
self.stop_func = lambda: self.runner.remove_game_data(appid=self.appid)
def get_steam_data_path(self):
"""Return path of Steam files"""
data_path = self.runner.get_game_path_from_appid(appid=self.appid)
if not data_path or not os.path.exists(data_path):
logger.info("No path found for Steam game %s", self.appid)
return ""
return os.path.abspath(
os.path.join(data_path, self.steam_rel_path)
)
def _monitor_steam_game_install(self):
if self.cancelled:
return False
states = get_app_state_log(
self.runner.steam_data_dir, self.appid, self.install_start_time
)
if states and states != self.prev_states:
self.state = states[-1].split(",")[-1]
logger.debug("Steam installation status: %s", states)
self.emit("steam-state-changed", self.state) # Broadcast new state to listeners
self.prev_states = states
logger.debug(self.state)
logger.debug(states)
if self.state == "Fully Installed":
logger.info("Steam game %s has been installed successfully", self.appid)
self.emit("steam-game-installed", self.appid)
return False
return True
steam_rel_path
property
readonly
¶
Return the relative path for data files
__init__(self, steam_uri, file_id)
special
¶
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
steam_uri |
Colon separated game info containing: - $STEAM - The Steam appid - The relative path of files to retrieve |
required | |
file_id |
The lutris installer internal id for the game files |
required |
Source code in lutris/installer/steam_installer.py
def __init__(self, steam_uri, file_id):
"""
Params:
steam_uri: Colon separated game info containing:
- $STEAM
- The Steam appid
- The relative path of files to retrieve
file_id: The lutris installer internal id for the game files
"""
super().__init__()
self.steam_poll = None
self.prev_states = [] # Previous states for the Steam installer
self.state = ""
self.install_start_time = None
self.steam_uri = steam_uri
self.stop_func = None
self.cancelled = False
self._runner = None
self.file_id = file_id
try:
_steam, appid, path = self.steam_uri.split(":", 2)
except ValueError as err:
raise ScriptingError(_("Malformed steam path: %s") % self.steam_uri) from err
self.appid = appid
self.path = path
self.platform = "linux"
self.runner = steam.steam()
get_steam_data_path(self)
¶
Return path of Steam files
Source code in lutris/installer/steam_installer.py
def get_steam_data_path(self):
"""Return path of Steam files"""
data_path = self.runner.get_game_path_from_appid(appid=self.appid)
if not data_path or not os.path.exists(data_path):
logger.info("No path found for Steam game %s", self.appid)
return ""
return os.path.abspath(
os.path.join(data_path, self.steam_rel_path)
)
install_steam_game(self)
¶
Launch installation of a steam game
Source code in lutris/installer/steam_installer.py
def install_steam_game(self):
"""Launch installation of a steam game"""
if self.runner.get_game_path_from_appid(appid=self.appid):
logger.info("Steam game %s is already installed", self.appid)
self.emit("steam-game-installed", self.appid)
else:
logger.debug("Installing steam game %s", self.appid)
self.runner.config = LutrisConfig(runner_slug=self.runner.name)
AsyncCall(self.runner.install_game, self.on_steam_game_installed, self.appid)
self.install_start_time = time.localtime()
self.steam_poll = GLib.timeout_add(2000, self._monitor_steam_game_install)
self.stop_func = lambda: self.runner.remove_game_data(appid=self.appid)
on_steam_game_installed(_data, error)
staticmethod
¶
Callback for Steam game installer, mostly for error handling since install progress is handled by _monitor_steam_game_install
Source code in lutris/installer/steam_installer.py
@staticmethod
def on_steam_game_installed(_data, error):
"""Callback for Steam game installer, mostly for error handling
since install progress is handled by _monitor_steam_game_install
"""
if error:
raise ScriptingError(str(error))
migrations
special
¶
MIGRATIONS
¶
MIGRATION_VERSION
¶
get_migration_module(migration_name)
¶
Source code in lutris/migrations/__init__.py
def get_migration_module(migration_name):
return importlib.import_module("lutris.migrations.%s" % migration_name)
migrate()
¶
Source code in lutris/migrations/__init__.py
def migrate():
current_version = int(settings.read_setting("migration_version") or 0)
if current_version >= MIGRATION_VERSION:
return
for i in range(current_version, MIGRATION_VERSION):
for migration_name in MIGRATIONS[i]:
logger.info("Running migration: %s", migration_name)
migration = get_migration_module(migration_name)
migration.migrate()
settings.write_setting("migration_version", MIGRATION_VERSION)
mess_to_mame
¶
Migrate MESS games to MAME
migrate()
¶
Run migration
Source code in lutris/migrations/mess_to_mame.py
def migrate():
"""Run migration"""
for pga_game in get_games():
game = Game(pga_game["id"])
if game.runner_name != "mess":
continue
if "mess" in game.config.game_level:
game.config.game_level["mame"] = game.config.game_level.pop("mess")
game.runner_name = "mame"
game.save()
migrate_banners
¶
Migrate banners from .local/share/lutris to .cache/lutris
migrate()
¶
Source code in lutris/migrations/migrate_banners.py
def migrate():
dest_dir = settings.BANNER_PATH
src_dir = os.path.join(settings.DATA_DIR, "banners")
try:
# init_lutris() creates the new banners directrory
if os.path.isdir(src_dir) and os.path.isdir(dest_dir):
for filename in os.listdir(src_dir):
src_file = os.path.join(src_dir, filename)
dest_file = os.path.join(dest_dir, filename)
if not os.path.exists(dest_file):
os.rename(src_file, dest_file)
else:
os.unlink(src_file)
if not os.listdir(src_dir):
os.rmdir(src_dir)
except OSError as ex:
logger.exception("Failed to migrate banners: %s", ex)
migrate_hidden_ids
¶
Move hidden games from settings to database
get_hidden_ids()
¶
Return a list of game IDs to be excluded from the library view
Source code in lutris/migrations/migrate_hidden_ids.py
def get_hidden_ids():
"""Return a list of game IDs to be excluded from the library view"""
# Load the ignore string and filter out empty strings to prevent issues
ignores_raw = settings.read_setting("library_ignores", section="lutris", default="").split(",")
ignores = [ignore for ignore in ignores_raw if not ignore == ""]
# Turn the strings into integers
return [int(game_id) for game_id in ignores]
migrate()
¶
Run migration
Source code in lutris/migrations/migrate_hidden_ids.py
def migrate():
"""Run migration"""
try:
game_ids = get_hidden_ids()
except:
print("Failed to read hidden game IDs")
return []
for game_id in game_ids:
game = Game(game_id)
game.set_hidden(True)
settings.write_setting("library_ignores", '', section="lutris")
migrate_steam_appids
¶
Set service ID for Steam games
migrate()
¶
Run migration
Source code in lutris/migrations/migrate_steam_appids.py
def migrate():
"""Run migration"""
for game in get_games():
if not game.get("steamid"):
continue
if game["runner"] and game["runner"] != "steam":
continue
print("Migrating Steam game %s" % game["name"])
sql.db_update(
settings.PGA_DB,
"games",
{"service": "steam", "service_id": game["steamid"]},
{"id": game["id"]}
)
runner_interpreter
¶
Transform runner parameters to data usable for runtime execution
export_bash_script(runner, gameplay_info, script_path)
¶
Convert runner configuration into a bash script
Source code in lutris/runner_interpreter.py
def export_bash_script(runner, gameplay_info, script_path):
"""Convert runner configuration into a bash script"""
if getattr(runner, 'prelaunch', None) is not None:
runner.prelaunch()
command, env = get_launch_parameters(runner, gameplay_info)
# Override TERM otherwise the script might not run
env["TERM"] = "xterm"
script_content = "#!/bin/bash\n\n\n"
script_content += "# Environment variables\n"
for name, value in env.items():
script_content += 'export %s="%s"\n' % (name, value)
script_content += "\n# Command\n"
script_content += " ".join([shlex.quote(c) for c in command])
with open(script_path, "w", encoding='utf-8') as script_file:
script_file.write(script_content)
os.chmod(script_path, os.stat(script_path).st_mode | stat.S_IEXEC)
get_gamescope_args(launch_arguments, system_config)
¶
Insert gamescope at the start of the launch arguments
Source code in lutris/runner_interpreter.py
def get_gamescope_args(launch_arguments, system_config):
"""Insert gamescope at the start of the launch arguments"""
launch_arguments.insert(0, "--")
launch_arguments.insert(0, "-f")
if system_config.get("gamescope_output_res"):
output_width, output_height = system_config["gamescope_output_res"].lower().split("x")
launch_arguments.insert(0, output_height)
launch_arguments.insert(0, "-H")
launch_arguments.insert(0, output_width)
launch_arguments.insert(0, "-W")
if system_config.get("gamescope_game_res"):
game_width, game_height = system_config["gamescope_game_res"].lower().split("x")
launch_arguments.insert(0, game_height)
launch_arguments.insert(0, "-h")
launch_arguments.insert(0, game_width)
launch_arguments.insert(0, "-w")
launch_arguments.insert(0, "gamescope")
return launch_arguments
get_launch_parameters(runner, gameplay_info)
¶
Source code in lutris/runner_interpreter.py
def get_launch_parameters(runner, gameplay_info):
system_config = runner.system_config
launch_arguments = gameplay_info["command"]
env = {
"DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1": "1"
}
# Steam compatibility
if os.environ.get("SteamAppId"):
logger.info("Game launched from steam (AppId: %s)", os.environ["SteamAppId"])
env["LC_ALL"] = ""
# Optimus
optimus = system_config.get("optimus")
if optimus == "primusrun" and system.find_executable("primusrun"):
launch_arguments.insert(0, "primusrun")
elif optimus == "optirun" and system.find_executable("optirun"):
launch_arguments.insert(0, "virtualgl")
launch_arguments.insert(0, "-b")
launch_arguments.insert(0, "optirun")
elif optimus == "pvkrun" and system.find_executable("pvkrun"):
launch_arguments.insert(0, "pvkrun")
mango_args, mango_env = get_mangohud_conf(system_config)
if mango_args:
launch_arguments = mango_args + launch_arguments
env.update(mango_env)
# Libstrangle
fps_limit = system_config.get("fps_limit") or ""
if fps_limit:
strangle_cmd = system.find_executable("strangle")
if strangle_cmd:
launch_arguments = [strangle_cmd, fps_limit] + launch_arguments
else:
logger.warning("libstrangle is not available on this system, FPS limiter disabled")
prefix_command = system_config.get("prefix_command") or ""
if prefix_command:
launch_arguments = (shlex.split(os.path.expandvars(prefix_command)) + launch_arguments)
single_cpu = system_config.get("single_cpu") or False
if single_cpu:
limit_cpu_count = system_config.get("limit_cpu_count")
if limit_cpu_count and limit_cpu_count.isnumeric():
limit_cpu_count = int(limit_cpu_count)
else:
limit_cpu_count = 1
limit_cpu_count = max(1, limit_cpu_count)
logger.info("The game will run on %d CPU core(s)", limit_cpu_count)
launch_arguments.insert(0, "0-%d" % (limit_cpu_count - 1))
launch_arguments.insert(0, "-c")
launch_arguments.insert(0, "taskset")
env.update(runner.get_env())
env.update(gameplay_info.get("env") or {})
# Set environment variables dependent on gameplay info
# LD_PRELOAD
ld_preload = gameplay_info.get("ld_preload")
if ld_preload:
env["LD_PRELOAD"] = ld_preload
# LD_LIBRARY_PATH
game_ld_library_path = gameplay_info.get("ld_library_path")
if game_ld_library_path:
ld_library_path = env.get("LD_LIBRARY_PATH")
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
game_ld_library_path, ld_library_path]))
# Feral gamemode
gamemode = system_config.get("gamemode") and LINUX_SYSTEM.gamemode_available()
if gamemode:
launch_arguments.insert(0, "gamemoderun")
# Gamescope
gamescope = system_config.get("gamescope") and system.find_executable("gamescope")
if gamescope:
launch_arguments = get_gamescope_args(launch_arguments, system_config)
return launch_arguments, env
get_mangohud_conf(system_config)
¶
Return correct launch arguments and environment variables for Mangohud.
Source code in lutris/runner_interpreter.py
def get_mangohud_conf(system_config):
"""Return correct launch arguments and environment variables for Mangohud."""
env = {"MANGOHUD": "1"}
mango_args = []
mangohud = system_config.get("mangohud") or ""
if mangohud and system.find_executable("mangohud"):
if mangohud == "gl64":
mango_args = ["mangohud"]
env["MANGOHUD_DLSYM"] = "1"
elif mangohud == "gl32":
mango_args = ["mangohud.x86"]
env["MANGOHUD_DLSYM"] = "1"
else:
mango_args = ["mangohud"]
return mango_args, env
runners
special
¶
Runner loaders
ADDON_RUNNERS
¶
RUNNER_NAMES
¶
RUNNER_PLATFORMS
¶
__all__
special
¶
InvalidRunner (Exception)
¶
Source code in lutris/runners/__init__.py
class InvalidRunner(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message
__init__(self, message)
special
¶
Source code in lutris/runners/__init__.py
def __init__(self, message):
super().__init__(message)
self.message = message
NonInstallableRunnerError (Exception)
¶
Source code in lutris/runners/__init__.py
class NonInstallableRunnerError(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message
__init__(self, message)
special
¶
Source code in lutris/runners/__init__.py
def __init__(self, message):
super().__init__(message)
self.message = message
RunnerInstallationError (Exception)
¶
Source code in lutris/runners/__init__.py
class RunnerInstallationError(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message
__init__(self, message)
special
¶
Source code in lutris/runners/__init__.py
def __init__(self, message):
super().__init__(message)
self.message = message
get_installed(sort=True)
¶
Return a list of installed runners (class instances).
Source code in lutris/runners/__init__.py
def get_installed(sort=True):
"""Return a list of installed runners (class instances)."""
installed = []
for runner_name in __all__:
runner = import_runner(runner_name)()
if runner.is_installed():
installed.append(runner)
return sorted(installed) if sort else installed
get_platforms()
¶
Return a dictionary of all supported platforms with their runners
Source code in lutris/runners/__init__.py
def get_platforms():
"""Return a dictionary of all supported platforms with their runners"""
platforms = defaultdict(list)
for runner_name in __all__:
runner = import_runner(runner_name)()
for platform in runner.platforms:
platforms[platform].append(runner_name)
return platforms
get_runner_module(runner_name)
¶
Source code in lutris/runners/__init__.py
def get_runner_module(runner_name):
if runner_name not in __all__:
raise InvalidRunner("Invalid runner name '%s'" % runner_name)
return __import__("lutris.runners.%s" % runner_name, globals(), locals(), [runner_name], 0)
get_runner_names()
¶
Source code in lutris/runners/__init__.py
def get_runner_names():
return {
runner: import_runner(runner)().human_name for runner in __all__
}
import_runner(runner_name)
¶
Dynamically import a runner class.
Source code in lutris/runners/__init__.py
def import_runner(runner_name):
"""Dynamically import a runner class."""
if runner_name in ADDON_RUNNERS:
return ADDON_RUNNERS[runner_name]
runner_module = get_runner_module(runner_name)
if not runner_module:
return None
return getattr(runner_module, runner_name)
import_task(runner, task)
¶
Return a runner task.
Source code in lutris/runners/__init__.py
def import_task(runner, task):
"""Return a runner task."""
runner_module = get_runner_module(runner)
if not runner_module:
return None
return getattr(runner_module, task)
inject_runners(runners)
¶
Source code in lutris/runners/__init__.py
def inject_runners(runners):
for runner_name in runners:
ADDON_RUNNERS[runner_name] = runners[runner_name]
__all__.append(runner_name)
atari800
¶
atari800 (Runner)
¶
Source code in lutris/runners/atari800.py
class atari800(Runner):
human_name = _("Atari800")
platforms = [_("Atari 8bit computers")] # FIXME try to determine the actual computer used
runner_executable = "atari800/bin/atari800"
bios_url = "http://kent.dl.sourceforge.net/project/atari800/ROM/Original%20XL%20ROM/xf25.zip"
description = _("Atari 400, 800 and XL emulator")
bios_checksums = {
"xlxe_rom": "06daac977823773a3eea3422fd26a703",
"basic_rom": "0bac0c6a50104045d902df4503a4c30b",
"osa_rom": "",
"osb_rom": "a3e8d617c95d08031fe1b20d541434b2",
"5200_rom": "",
}
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"help": _(
"The game data, commonly called a ROM image. \n"
"Supported formats: ATR, XFD, DCM, ATR.GZ, XFD.GZ "
"and PRO."
),
}
]
runner_options = [
{
"option":
"bios_path",
"type":
"directory_chooser",
"label":
_("BIOS location"),
"help": _(
"A folder containing the Atari 800 BIOS files.\n"
"They are provided by Lutris so you shouldn't have to "
"change this."
),
},
{
"option":
"machine",
"type":
"choice",
"choices": [
(_("Emulate Atari 800"), "atari"),
(_("Emulate Atari 800 XL"), "xl"),
(_("Emulate Atari 320 XE (Compy Shop)"), "320xe"),
(_("Emulate Atari 320 XE (Rambo)"), "rambo"),
(_("Emulate Atari 5200"), "5200"),
],
"default":
"atari",
"label":
_("Machine"),
},
{
"option": "fullscreen",
"type": "bool",
"default": False,
"label": _("Fullscreen"),
},
{
"option": "resolution",
"type": "choice",
"choices": get_resolutions(),
"default": "desktop",
"label": _("Fullscreen resolution"),
},
]
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args): # pylint: disable=unused-argument
config_path = system.create_folder("~/.atari800")
bios_archive = os.path.join(config_path, "atari800-bioses.zip")
dlg = DownloadDialog(self.bios_url, bios_archive)
dlg.run()
if not system.path_exists(bios_archive):
ErrorDialog(_("Could not download Atari 800 BIOS archive"))
return
extract.extract_archive(bios_archive, config_path)
os.remove(bios_archive)
config = LutrisConfig(runner_slug="atari800")
config.raw_runner_config.update({"bios_path": config_path})
config.save()
if callback:
callback()
super().install(version, downloader, on_runner_installed)
def find_good_bioses(self, bios_path):
""" Check for correct bios files """
good_bios = {}
for filename in os.listdir(bios_path):
real_hash = system.get_md5_hash(os.path.join(bios_path, filename))
for bios_file, checksum in self.bios_checksums.items():
if real_hash == checksum:
logging.debug("%s Checksum : OK", filename)
good_bios[bios_file] = filename
return good_bios
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("fullscreen"):
arguments.append("-fullscreen")
else:
arguments.append("-windowed")
resolution = self.runner_config.get("resolution")
if resolution:
if resolution == "desktop":
width, height = display.DISPLAY_MANAGER.get_current_resolution()
else:
width, height = resolution.split("x")
arguments += ["-fs-width", "%s" % width, "-fs-height", "%s" % height]
if self.runner_config.get("machine"):
arguments.append("-%s" % self.runner_config["machine"])
bios_path = self.runner_config.get("bios_path")
if not system.path_exists(bios_path):
return {"error": "NO_BIOS"}
good_bios = self.find_good_bioses(bios_path)
for bios, filename in good_bios.items():
arguments.append("-%s" % bios)
arguments.append(os.path.join(bios_path, filename))
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
bios_checksums
¶
bios_url
¶
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
find_good_bioses(self, bios_path)
¶
Check for correct bios files
Source code in lutris/runners/atari800.py
def find_good_bioses(self, bios_path):
""" Check for correct bios files """
good_bios = {}
for filename in os.listdir(bios_path):
real_hash = system.get_md5_hash(os.path.join(bios_path, filename))
for bios_file, checksum in self.bios_checksums.items():
if real_hash == checksum:
logging.debug("%s Checksum : OK", filename)
good_bios[bios_file] = filename
return good_bios
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/atari800.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args): # pylint: disable=unused-argument
config_path = system.create_folder("~/.atari800")
bios_archive = os.path.join(config_path, "atari800-bioses.zip")
dlg = DownloadDialog(self.bios_url, bios_archive)
dlg.run()
if not system.path_exists(bios_archive):
ErrorDialog(_("Could not download Atari 800 BIOS archive"))
return
extract.extract_archive(bios_archive, config_path)
os.remove(bios_archive)
config = LutrisConfig(runner_slug="atari800")
config.raw_runner_config.update({"bios_path": config_path})
config.save()
if callback:
callback()
super().install(version, downloader, on_runner_installed)
play(self)
¶
Source code in lutris/runners/atari800.py
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("fullscreen"):
arguments.append("-fullscreen")
else:
arguments.append("-windowed")
resolution = self.runner_config.get("resolution")
if resolution:
if resolution == "desktop":
width, height = display.DISPLAY_MANAGER.get_current_resolution()
else:
width, height = resolution.split("x")
arguments += ["-fs-width", "%s" % width, "-fs-height", "%s" % height]
if self.runner_config.get("machine"):
arguments.append("-%s" % self.runner_config["machine"])
bios_path = self.runner_config.get("bios_path")
if not system.path_exists(bios_path):
return {"error": "NO_BIOS"}
good_bios = self.find_good_bioses(bios_path)
for bios, filename in good_bios.items():
arguments.append("-%s" % bios)
arguments.append(os.path.join(bios_path, filename))
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
get_resolutions()
¶
Source code in lutris/runners/atari800.py
def get_resolutions():
try:
screen_resolutions = [(resolution, resolution) for resolution in display.DISPLAY_MANAGER.get_resolutions()]
except OSError:
screen_resolutions = []
screen_resolutions.insert(0, (_("Desktop resolution"), "desktop"))
return screen_resolutions
commands
special
¶
dosbox
¶
DOSBox installer commands
dosexec(config_file=None, executable=None, args=None, close_on_exit=True, working_dir=None)
¶
Execute Dosbox with given config_file.
Source code in lutris/runners/commands/dosbox.py
def dosexec(config_file=None, executable=None, args=None, close_on_exit=True, working_dir=None):
"""Execute Dosbox with given config_file."""
if config_file:
run_with = "config {}".format(config_file)
if not working_dir:
working_dir = os.path.dirname(config_file)
elif executable:
run_with = "executable {}".format(executable)
if not working_dir:
working_dir = os.path.dirname(executable)
else:
raise ValueError("Neither a config file or an executable were provided")
logger.debug("Running dosbox with %s", run_with)
working_dir = system.create_folder(working_dir)
dosbox = import_runner("dosbox")
dosbox_runner = dosbox()
command = [dosbox_runner.get_executable()]
if config_file:
command += ["-conf", config_file]
if executable:
if not system.path_exists(executable):
raise OSError("Can't find file {}".format(executable))
command += [executable]
if args:
command += args.split()
if close_on_exit:
command.append("-exit")
system.execute(command, cwd=working_dir, env=runtime.get_env())
makeconfig(path, drives, commands)
¶
Source code in lutris/runners/commands/dosbox.py
def makeconfig(path, drives, commands):
system.create_folder(os.path.dirname(path))
with open(path, "w", encoding='utf-8') as config_file:
config_file.write("[autoexec]\n")
for drive in drives:
config_file.write('mount {} "{}"\n'.format(drive, drives[drive]))
for command in commands:
config_file.write("{}\n".format(command))
wine
¶
Wine commands for installers
create_prefix(prefix, wine_path=None, arch='win64', overrides=None, install_gecko=None, install_mono=None)
¶
Create a new Wine prefix.
Source code in lutris/runners/commands/wine.py
def create_prefix( # noqa: C901
prefix,
wine_path=None,
arch=WINE_DEFAULT_ARCH,
overrides=None,
install_gecko=None,
install_mono=None,
):
"""Create a new Wine prefix."""
# pylint: disable=too-many-locals
if overrides is None:
overrides = {}
if not prefix:
raise ValueError("No Wine prefix path given")
logger.info("Creating a %s prefix in %s", arch, prefix)
# Follow symlinks, don't delete existing ones as it would break some setups
if os.path.islink(prefix):
prefix = os.readlink(prefix)
# Avoid issue of 64bit Wine refusing to create win32 prefix
# over an existing empty folder.
if os.path.isdir(prefix) and not os.listdir(prefix):
os.rmdir(prefix)
if not wine_path:
wine = import_runner("wine")
wine_path = wine().get_executable()
if not wine_path:
logger.error("Wine not found, can't create prefix")
return
wineboot_path = os.path.join(os.path.dirname(wine_path), "wineboot")
if not system.path_exists(wineboot_path):
logger.error(
"No wineboot executable found in %s, "
"your wine installation is most likely broken",
wine_path,
)
return
wineenv = {
"WINEARCH": arch,
"WINEPREFIX": prefix,
"WINEDLLOVERRIDES": get_overrides_env(overrides),
"WINE_MONO_CACHE_DIR": os.path.join(os.path.dirname(os.path.dirname(wine_path)), "mono"),
"WINE_GECKO_CACHE_DIR": os.path.join(os.path.dirname(os.path.dirname(wine_path)), "gecko"),
}
if install_gecko == "False":
wineenv["WINE_SKIP_GECKO_INSTALLATION"] = "1"
overrides["mshtml"] = "disabled"
if install_mono == "False":
wineenv["WINE_SKIP_MONO_INSTALLATION"] = "1"
overrides["mscoree"] = "disabled"
system.execute([wineboot_path], env=wineenv)
for loop_index in range(1000):
time.sleep(0.5)
if system.path_exists(os.path.join(prefix, "user.reg")):
break
if loop_index == 60:
logger.warning("Wine prefix creation is taking longer than expected...")
if not os.path.exists(os.path.join(prefix, "user.reg")):
logger.error("No user.reg found after prefix creation. " "Prefix might not be valid")
return
logger.info("%s Prefix created in %s", arch, prefix)
prefix_manager = WinePrefixManager(prefix)
prefix_manager.setup_defaults()
delete_registry_key(key, wine_path=None, prefix=None, arch='win64')
¶
Deletes a registry key from a Wine prefix
Source code in lutris/runners/commands/wine.py
def delete_registry_key(key, wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH):
"""Deletes a registry key from a Wine prefix"""
wineexec(
"regedit",
args='/S /D "%s"' % key,
wine_path=wine_path,
prefix=prefix,
arch=arch,
blocking=True,
)
eject_disc(wine_path, prefix)
¶
Use Wine to eject a drive
Source code in lutris/runners/commands/wine.py
def eject_disc(wine_path, prefix):
"""Use Wine to eject a drive"""
wineexec("eject", prefix=prefix, wine_path=wine_path, args="-a")
install_cab_component(cabfile, component, wine_path=None, prefix=None, arch=None)
¶
Install a component from a cabfile in a prefix
Source code in lutris/runners/commands/wine.py
def install_cab_component(cabfile, component, wine_path=None, prefix=None, arch=None):
"""Install a component from a cabfile in a prefix"""
cab_installer = CabInstaller(prefix, wine_path=wine_path, arch=arch)
files = cab_installer.extract_from_cab(cabfile, component)
registry_files = cab_installer.get_registry_files(files)
for registry_file, _arch in registry_files:
set_regedit_file(registry_file, wine_path=wine_path, prefix=prefix, arch=_arch)
cab_installer.cleanup()
open_wine_terminal(terminal, wine_path, prefix, env)
¶
Source code in lutris/runners/commands/wine.py
def open_wine_terminal(terminal, wine_path, prefix, env):
aliases = {
"wine": wine_path,
"winecfg": wine_path + "cfg",
"wineserver": wine_path + "server",
"wineboot": wine_path + "boot",
}
env["WINEPREFIX"] = prefix
shell_command = get_shell_command(prefix, env, aliases)
terminal = terminal or linux.get_default_terminal()
system.execute([terminal, "-e", shell_command])
set_regedit(path, key, value='', type='REG_SZ', wine_path=None, prefix=None, arch='win64')
¶
Add keys to the windows registry.
Path is something like HKEY_CURRENT_USER/Software/Wine/Direct3D
Source code in lutris/runners/commands/wine.py
def set_regedit(
path,
key,
value="",
type="REG_SZ", # pylint: disable=redefined-builtin
wine_path=None,
prefix=None,
arch=WINE_DEFAULT_ARCH,
):
"""Add keys to the windows registry.
Path is something like HKEY_CURRENT_USER/Software/Wine/Direct3D
"""
formatted_value = {
"REG_SZ": '"%s"' % value,
"REG_DWORD": "dword:" + value,
"REG_BINARY": "hex:" + value.replace(" ", ","),
"REG_MULTI_SZ": "hex(2):" + value,
"REG_EXPAND_SZ": "hex(7):" + value,
}
# Make temporary reg file
reg_path = os.path.join(settings.CACHE_DIR, "winekeys.reg")
with open(reg_path, "w", encoding='utf-8') as reg_file:
reg_file.write('REGEDIT4\n\n[%s]\n"%s"=%s\n' % (path, key, formatted_value[type]))
logger.debug("Setting [%s]:%s=%s", path, key, formatted_value[type])
set_regedit_file(reg_path, wine_path=wine_path, prefix=prefix, arch=arch)
os.remove(reg_path)
set_regedit_file(filename, wine_path=None, prefix=None, arch='win64')
¶
Apply a regedit file to the Windows registry.
Source code in lutris/runners/commands/wine.py
def set_regedit_file(filename, wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH):
"""Apply a regedit file to the Windows registry."""
if arch == "win64" and wine_path and system.path_exists(wine_path + "64"):
# Use wine64 by default if set to a 64bit prefix. Using regular wine
# will prevent some registry keys from being created. Most likely to be
# a bug in Wine. see: https://github.com/lutris/lutris/issues/804
wine_path = wine_path + "64"
wineexec(
"regedit",
args="/S '%s'" % filename,
wine_path=wine_path,
prefix=prefix,
arch=arch,
blocking=True,
)
winecfg(wine_path=None, prefix=None, arch='win64', config=None, env=None)
¶
Execute winecfg.
Source code in lutris/runners/commands/wine.py
def winecfg(wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH, config=None, env=None):
"""Execute winecfg."""
if not wine_path:
logger.debug("winecfg: Reverting to default wine")
wine = import_runner("wine")
wine_path = wine().get_executable()
winecfg_path = os.path.join(os.path.dirname(wine_path), "winecfg")
logger.debug("winecfg: %s", winecfg_path)
return wineexec(
None,
prefix=prefix,
winetricks_wine=winecfg_path,
wine_path=winecfg_path,
arch=arch,
config=config,
env=env,
include_processes=["winecfg.exe"],
)
wineexec(executable, args='', wine_path=None, prefix=None, arch=None, working_dir=None, winetricks_wine='', blocking=False, config=None, include_processes=None, exclude_processes=None, disable_runtime=False, env=None, overrides=None)
¶
Execute a Wine command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
executable |
str |
wine program to run, pass None to run wine itself |
required |
args |
str |
program arguments |
'' |
wine_path |
str |
path to the wine version to use |
None |
prefix |
str |
path to the wine prefix to use |
None |
arch |
str |
wine architecture of the prefix |
None |
working_dir |
str |
path to the working dir for the process |
None |
winetricks_wine |
str |
path to the wine version used by winetricks |
'' |
blocking |
bool |
if true, do not run the process in a thread |
False |
config |
LutrisConfig |
LutrisConfig object for the process context |
None |
watch |
list |
list of process names to monitor (even when in a ignore list) |
required |
Returns:
| Type | Description |
|---|---|
Process results if the process is running in blocking mode or MonitoredCommand instance otherwise. |
Source code in lutris/runners/commands/wine.py
def wineexec( # noqa: C901
executable,
args="",
wine_path=None,
prefix=None,
arch=None,
working_dir=None,
winetricks_wine="",
blocking=False,
config=None,
include_processes=None,
exclude_processes=None,
disable_runtime=False,
env=None,
overrides=None,
):
"""
Execute a Wine command.
Args:
executable (str): wine program to run, pass None to run wine itself
args (str): program arguments
wine_path (str): path to the wine version to use
prefix (str): path to the wine prefix to use
arch (str): wine architecture of the prefix
working_dir (str): path to the working dir for the process
winetricks_wine (str): path to the wine version used by winetricks
blocking (bool): if true, do not run the process in a thread
config (LutrisConfig): LutrisConfig object for the process context
watch (list): list of process names to monitor (even when in a ignore list)
Returns:
Process results if the process is running in blocking mode or
MonitoredCommand instance otherwise.
"""
if env is None:
env = {}
if exclude_processes is None:
exclude_processes = []
if include_processes is None:
include_processes = []
executable = str(executable) if executable else ""
if isinstance(include_processes, str):
include_processes = shlex.split(include_processes)
if isinstance(exclude_processes, str):
exclude_processes = shlex.split(exclude_processes)
wine = import_runner("wine")()
if not wine_path:
wine_path = wine.get_executable()
if not wine_path:
raise RuntimeError("Wine is not installed")
if not working_dir:
if os.path.isfile(executable):
working_dir = os.path.dirname(executable)
executable, _args, working_dir = get_real_executable(executable, working_dir)
if _args:
args = '{} "{}"'.format(_args[0], _args[1])
# Create prefix if necessary
if arch not in ("win32", "win64"):
arch = detect_arch(prefix, wine_path)
if not detect_prefix_arch(prefix):
wine_bin = winetricks_wine if winetricks_wine else wine_path
create_prefix(prefix, wine_path=wine_bin, arch=arch)
wineenv = {"WINEARCH": arch}
if winetricks_wine:
wineenv["WINE"] = winetricks_wine
else:
wineenv["WINE"] = wine_path
if prefix:
wineenv["WINEPREFIX"] = prefix
wine_system_config = config.system_config if config else wine.system_config
disable_runtime = disable_runtime or wine_system_config["disable_runtime"]
if use_lutris_runtime(wine_path=wineenv["WINE"], force_disable=disable_runtime):
if WINE_DIR in wine_path:
wine_root_path = os.path.dirname(os.path.dirname(wine_path))
elif WINE_DIR in winetricks_wine:
wine_root_path = os.path.dirname(os.path.dirname(winetricks_wine))
else:
wine_root_path = None
wineenv["LD_LIBRARY_PATH"] = ":".join(
runtime.get_paths(
prefer_system_libs=wine_system_config["prefer_system_libs"],
wine_path=wine_root_path,
)
)
if overrides:
wineenv["WINEDLLOVERRIDES"] = get_overrides_env(overrides)
baseenv = wine.get_env()
baseenv.update(wineenv)
baseenv.update(env)
command_parameters = [wine_path]
if executable:
command_parameters.append(executable)
command_parameters += split_arguments(args)
wine.prelaunch()
if blocking:
return system.execute(command_parameters, env=wineenv, cwd=working_dir)
command = MonitoredCommand(
command_parameters,
runner=wine,
env=baseenv,
cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes,
)
command.start()
return command
winekill(prefix, arch='win64', wine_path=None, env=None, initial_pids=None)
¶
Kill processes in Wine prefix.
Source code in lutris/runners/commands/wine.py
def winekill(prefix, arch=WINE_DEFAULT_ARCH, wine_path=None, env=None, initial_pids=None):
"""Kill processes in Wine prefix."""
initial_pids = initial_pids or []
if not wine_path:
wine = import_runner("wine")
wine_path = wine().get_executable()
wine_root = os.path.dirname(wine_path)
if not env:
env = {"WINEARCH": arch, "WINEPREFIX": prefix}
command = [os.path.join(wine_root, "wineserver"), "-k"]
logger.debug("Killing all wine processes: %s", command)
logger.debug("\tWine prefix: %s", prefix)
logger.debug("\tWine arch: %s", arch)
if initial_pids:
logger.debug("\tInitial pids: %s", initial_pids)
system.execute(command, env=env, quiet=True)
logger.debug("Waiting for wine processes to terminate")
# Wineserver needs time to terminate processes
num_cycles = 0
while True:
num_cycles += 1
running_processes = [pid for pid in initial_pids if system.path_exists("/proc/%s" % pid)]
if not running_processes:
break
if num_cycles > 20:
logger.warning(
"Some wine processes are still running: %s",
", ".join(running_processes),
)
break
time.sleep(0.1)
logger.debug("Done waiting.")
winetricks(app, prefix=None, arch=None, silent=True, wine_path=None, config=None, env=None, disable_runtime=False)
¶
Execute winetricks.
Source code in lutris/runners/commands/wine.py
def winetricks(
app,
prefix=None,
arch=None,
silent=True,
wine_path=None,
config=None,
env=None,
disable_runtime=False,
):
"""Execute winetricks."""
wine_config = config or LutrisConfig(runner_slug="wine")
winetricks_path = os.path.join(settings.RUNTIME_DIR, "winetricks/winetricks")
if (wine_config.runner_config.get("system_winetricks") or not system.path_exists(winetricks_path)):
winetricks_path = system.find_executable("winetricks")
if not winetricks_path:
raise RuntimeError("No installation of winetricks found")
if wine_path:
winetricks_wine = wine_path
else:
wine = import_runner("wine")
winetricks_wine = wine().get_executable()
if arch not in ("win32", "win64"):
arch = detect_arch(prefix, winetricks_wine)
args = app
if str(silent).lower() in ("yes", "on", "true"):
args = "--unattended " + args
return wineexec(
None,
prefix=prefix,
winetricks_wine=winetricks_wine,
wine_path=winetricks_path,
arch=arch,
args=args,
config=config,
env=env,
disable_runtime=disable_runtime,
)
dolphin
¶
Dolphin runner
dolphin (Runner)
¶
Source code in lutris/runners/dolphin.py
class dolphin(Runner):
description = _("GameCube and Wii emulator")
human_name = _("Dolphin")
platforms = [_("Nintendo GameCube"), _("Nintendo Wii")]
require_libs = ["libOpenGL.so.0", ]
runnable_alone = True
runner_executable = "dolphin/dolphin-emu"
game_options = [
{
"option": "main_file",
"type": "file",
"default_path": "game_path",
"label": _("ISO file"),
},
{
"option": "platform",
"type": "choice",
"label": _("Platform"),
"choices": ((_("Nintendo GameCube"), "0"), (_("Nintendo Wii"), "1")),
},
]
runner_options = [
{
"option": "nogui",
"type": "bool",
"label": _("No GUI"),
"default": False,
"help": _("Disable the graphical user interface."),
},
{
"option": "batch",
"type": "bool",
"label": _("Batch"),
"default": True,
"advanced": True,
"help": _("Exit Dolphin with emulator."),
},
{
"option": "user_directory",
"type": "directory_chooser",
"advanced": True,
"label": _("Custom Global User Directory"),
},
]
def get_platform(self):
selected_platform = self.game_config.get("platform")
if selected_platform:
return self.platforms[int(selected_platform)]
return ""
def play(self):
# Find the executable
executable = self.get_executable()
if self.runner_config.get("nogui"):
executable += "-nogui"
command = [executable]
# Batch isn't available in nogui
if self.runner_config.get("batch") and not self.runner_config.get("nogui"):
command.append("--batch")
# Custom Global User Directory
if self.runner_config.get("user_directory"):
command.append("-u")
command.append(self.runner_config["user_directory"])
# Retrieve the path to the file
iso = self.game_config.get("main_file") or ""
if not system.path_exists(iso):
return {"error": "FILE_NOT_FOUND", "file": iso}
command.extend(["-e", iso])
return {"command": command}
description
¶
game_options
¶
human_name
¶
platforms
¶
require_libs
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
get_platform(self)
¶
Source code in lutris/runners/dolphin.py
def get_platform(self):
selected_platform = self.game_config.get("platform")
if selected_platform:
return self.platforms[int(selected_platform)]
return ""
play(self)
¶
Source code in lutris/runners/dolphin.py
def play(self):
# Find the executable
executable = self.get_executable()
if self.runner_config.get("nogui"):
executable += "-nogui"
command = [executable]
# Batch isn't available in nogui
if self.runner_config.get("batch") and not self.runner_config.get("nogui"):
command.append("--batch")
# Custom Global User Directory
if self.runner_config.get("user_directory"):
command.append("-u")
command.append(self.runner_config["user_directory"])
# Retrieve the path to the file
iso = self.game_config.get("main_file") or ""
if not system.path_exists(iso):
return {"error": "FILE_NOT_FOUND", "file": iso}
command.extend(["-e", iso])
return {"command": command}
dosbox
¶
dosbox (Runner)
¶
Source code in lutris/runners/dosbox.py
class dosbox(Runner):
human_name = _("DOSBox")
description = _("MS-DOS emulator")
platforms = [_("MS-DOS")]
runnable_alone = True
runner_executable = "dosbox/bin/dosbox"
require_libs = ["libopusfile.so.0", ]
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("Main file"),
"help": _(
"The CONF, EXE, COM or BAT file to launch.\n"
"It can be left blank if the launch of the executable is "
"managed in the config file."
),
},
{
"option": "config_file",
"type": "file",
"label": _("Configuration file"),
"help": _(
"Start DOSBox with the options specified in this file. \n"
"It can have a section in which you can put commands "
"to execute on startup. Read DOSBox's documentation "
"for more information."
),
},
{
"option": "args",
"type": "string",
"label": _("Command line arguments"),
"help": _("Command line arguments used when launching DOSBox"),
"validator": shlex.split,
},
{
"option": "working_dir",
"type": "directory_chooser",
"label": _("Working directory"),
"help": _(
"The location where the game is run from.\n"
"By default, Lutris uses the directory of the "
"executable."
),
},
]
scaler_modes = [
(_("none"), "none"),
("normal2x", "normal2x"),
("normal3x", "normal3x"),
("hq2x", "hq2x"),
("hq3x", "hq3x"),
("advmame2x", "advmame2x"),
("advmame3x", "advmame3x"),
("2xsai", "2xsai"),
("super2xsai", "super2xsai"),
("supereagle", "supereagle"),
("advinterp2x", "advinterp2x"),
("advinterp3x", "advinterp3x"),
("tv2x", "tv2x"),
("tv3x", "tv3x"),
("rgb2x", "rgb2x"),
("rgb3x", "rgb3x"),
("scan2x", "scan2x"),
("scan3x", "scan3x"),
]
runner_options = [
{
"option":
"scaler",
"label":
_("Graphic scaler"),
"type":
"choice",
"choices":
scaler_modes,
"default":
"normal3x",
"help":
_("The algorithm used to scale up the game's base "
"resolution, resulting in different visual styles. "),
},
{
"option": "exit",
"label": _("Exit DOSBox with the game"),
"type": "bool",
"default": True,
"help": _("Shut down DOSBox when the game is quit."),
},
{
"option": "fullscreen",
"label": _("Open game in fullscreen"),
"type": "bool",
"default": False,
"help": _("Tells DOSBox to launch the game in fullscreen."),
},
]
def make_absolute(self, path):
"""Return a guaranteed absolute path"""
if not path:
return ""
if os.path.isabs(path):
return path
if self.game_data.get("directory"):
return os.path.join(self.game_data.get("directory"), path)
return ""
@property
def main_file(self):
return self.make_absolute(self.game_config.get("main_file"))
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
option = self.game_config.get("working_dir")
if option:
return os.path.expanduser(option)
if self.main_file:
return os.path.dirname(self.main_file)
return super().working_dir
def play(self):
main_file = self.main_file
if not system.path_exists(main_file):
return {"error": "FILE_NOT_FOUND", "file": main_file}
args = shlex.split(self.game_config.get("args") or "")
command = [self.get_executable()]
if main_file.endswith(".conf"):
command.append("-conf")
command.append(main_file)
else:
command.append(main_file)
# Options
if self.game_config.get("config_file"):
command.append("-conf")
command.append(self.make_absolute(self.game_config["config_file"]))
scaler = self.runner_config.get("scaler")
if scaler and scaler != "none":
command.append("-scaler")
command.append(self.runner_config["scaler"])
if self.runner_config.get("fullscreen"):
command.append("-fullscreen")
if self.runner_config.get("exit"):
command.append("-exit")
if args:
command.extend(args)
return {"command": command}
description
¶
game_options
¶
human_name
¶
main_file
property
readonly
¶
platforms
¶
require_libs
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
scaler_modes
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
make_absolute(self, path)
¶
Return a guaranteed absolute path
Source code in lutris/runners/dosbox.py
def make_absolute(self, path):
"""Return a guaranteed absolute path"""
if not path:
return ""
if os.path.isabs(path):
return path
if self.game_data.get("directory"):
return os.path.join(self.game_data.get("directory"), path)
return ""
play(self)
¶
Source code in lutris/runners/dosbox.py
def play(self):
main_file = self.main_file
if not system.path_exists(main_file):
return {"error": "FILE_NOT_FOUND", "file": main_file}
args = shlex.split(self.game_config.get("args") or "")
command = [self.get_executable()]
if main_file.endswith(".conf"):
command.append("-conf")
command.append(main_file)
else:
command.append(main_file)
# Options
if self.game_config.get("config_file"):
command.append("-conf")
command.append(self.make_absolute(self.game_config["config_file"]))
scaler = self.runner_config.get("scaler")
if scaler and scaler != "none":
command.append("-scaler")
command.append(self.runner_config["scaler"])
if self.runner_config.get("fullscreen"):
command.append("-fullscreen")
if self.runner_config.get("exit"):
command.append("-exit")
if args:
command.extend(args)
return {"command": command}
easyrpg
¶
easyrpg (Runner)
¶
Source code in lutris/runners/easyrpg.py
class easyrpg(Runner):
human_name = _("EasyRPG Player")
description = _("Runs RPG Maker 2000/2003 games")
platforms = [_("Linux")]
runnable_alone = True
entry_point_option = "project_path"
runner_executable = "easyrpg/easyrpg-player"
download_url = "https://easyrpg.org/downloads/player/0.7.0/easyrpg-player-0.7.0-linux.tar.gz"
game_options = [
{
"option": "project_path",
"type": "directory_chooser",
"label": _("Game directory"),
"help": _("Select the directory of the game. (required)")
},
{
"option": "encoding",
"type": "string",
"label": _("Encoding"),
"help": _(
"Instead of auto detecting the encoding or using the "
"one in RPG_RT.ini, the specified encoding is used. "
"Use 'auto' for automatic detection."
)
},
{
"option": "engine",
"type": "choice",
"label": _("Engine"),
"help": _("Disable auto detection of the simulated engine."),
"choices": [
(_("Auto"), ""),
(_("RPG Maker 2000 engine (v1.00 - v1.10)"), "rpg2k"),
(_("RPG Maker 2000 engine (v1.50 - v1.51)"), "rpg2kv150"),
(_("RPG Maker 2000 (English release) engine"), "rpg2ke"),
(_("RPG Maker 2003 engine (v1.00 - v1.04)"), "rpg2k3"),
(_("RPG Maker 2003 engine (v1.05 - v1.09a)"), "rpg2k3v105"),
(_("RPG Maker 2003 (English release) engine"), "rpg2k3e")
],
"default": ""
},
{
"option": "save_path",
"type": "directory_chooser",
"label": _("Save path"),
"help": _(
"Instead of storing save files in the game directory they "
"are stored in the specified path. The directory must exist."
)
},
{
"option": "new_game",
"type": "bool",
"label": _("New game"),
"help": _("Skip the title scene and start a new game directly."),
"default": False
},
{
"option": "load_game_id",
"type": "range",
"label": _("Load game ID"),
"help": _(
"Skip the title scene and load SaveXX.lsd. "
"Set to '0' to disable."
),
"min": 0,
"max": 99,
"default": 0
},
{
"option": "start_map_id",
"type": "range",
"label": _("Start map ID"),
"help": _(
"Overwrite the map used for new games and use "
"MapXXXX.lmu instead. Set to '0' to disable. "
"\n\nIncompatible with 'Load game ID'."
),
"min": 0,
"max": 9999,
"default": 0
},
{
"option": "start_position",
"type": "string",
"label": _("Start position"),
"help": _(
"Overwrite the party start position and "
"move the party to the specified position. "
"Provide two numbers separated by a space. "
"\n\nIncompatible with 'Load game ID'."
)
},
{
"option": "start_party",
"type": "string",
"label": _("Start party"),
"help": _(
"Overwrite the starting party members with "
"the actors with the specified IDs. Provide "
"one to four numbers separated by spaces. "
"\n\nIncompatible with 'Load game ID'."
)
},
{
"option": "battle_test",
"type": "string",
"label": _("Monster party"),
"help": _("Start a battle test with the specified monster party.")
},
{
"option": "record_input",
"type": "string",
"label": _("Record input"),
"help": _("Records all button input to the specified log file.")
},
{
"option": "replay_input",
"type": "file",
"label": _("Replay input"),
"help": _(
"Replays button input from the specified log file, "
"as generated by 'Record input'. If the RNG seed "
"and the state of the save file directory is also "
"the same as it was when the log was recorded, this "
"should reproduce an identical run to the one recorded."
)
},
]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"help": _("Start in fullscreen mode."),
"default": False
},
{
"option": "audio",
"type": "bool",
"label": _("Enable audio"),
"help": _(
"Switch off to disable audio "
"(in case you prefer your own music)."
),
"default": True
},
{
"option": "mouse",
"type": "bool",
"label": _("Enable mouse"),
"help": _(
"Use mouse click for decision and scroll wheel for lists."
),
"default": False
},
{
"option": "touch",
"type": "bool",
"label": _("Enable touch"),
"help": _("Use one/two finger tap for decision/cancel."),
"default": False
},
{
"option": "hide_title",
"type": "bool",
"label": _("Hide title"),
"help": _(
"Hide the title background image and center the command menu."
),
"default": False
},
{
"option": "vsync",
"type": "bool",
"label": _("Enable VSync"),
"help": _(
"Switch off to disable VSync and use the FPS limit. "
"VSync may or may not be supported on all platforms."
),
"default": True
},
{
"option": "fps_limit",
"type": "string",
"label": _("FPS limit"),
"help": _(
"Set a custom frames per second limit. If unspecified, "
"the default is 60 FPS. Set to '0' to disable the frame "
"limiter. This option may not be supported on all platforms."
)
},
{
"option": "show_fps",
"type": "choice",
"label": _("Show FPS"),
"help": _("Enable frames per second counter."),
"choices": [
(_("Disabled"), "off"),
(_("Fullscreen & title bar"), "on"),
(_("Fullscreen, title bar & window"), "full")
],
"default": "off"
},
{
"option": "seed",
"type": "string",
"label": _("RNG seed"),
"help": _("Seeds the random number generator")
},
{
"option": "test_play",
"type": "bool",
"label": _("Test play"),
"help": _("Enable TestPlay mode."),
"default": False
},
{
"option": "rtp",
"type": "bool",
"label": _("Enable RTP"),
"help": _(
"Switch off to disable support for the Runtime Package (RTP)."
),
"default": True
},
{
"option": "rpg2k_rtp_path",
"type": "directory_chooser",
"label": _("RPG2000 RTP location"),
"help": _(
"Full path to a directory containing an "
"extracted RPG Maker 2000 Run-Time-Package (RTP)."
)
},
{
"option": "rpg2k3_rtp_path",
"type": "directory_chooser",
"label": _("RPG2003 RTP location"),
"help": _(
"Full path to a directory containing an "
"extracted RPG Maker 2003 Run-Time-Package (RTP)."
)
},
{
"option": "rpg_rtp_path",
"type": "directory_chooser",
"label": _("Fallback RTP location"),
"help": _("Full path to a directory containing a combined RTP.")
},
]
@property
def game_path(self):
game_path = self.game_data.get("directory")
if game_path:
return game_path
# Default to the directory of the entry point
entry_point = self.game_config.get(self.entry_point_option)
if entry_point:
return path.expanduser(entry_point)
return ""
def get_env(self, os_env=False):
env = super().get_env(os_env)
rpg2k_rtp_path = self.runner_config.get("rpg2k_rtp_path")
if rpg2k_rtp_path:
env["RPG2K_RTP_PATH"] = rpg2k_rtp_path
rpg2k3_rtp_path = self.runner_config.get("rpg2k3_rtp_path")
if rpg2k3_rtp_path:
env["RPG2K3_RTP_PATH"] = rpg2k3_rtp_path
rpg_rtp_path = self.runner_config.get("rpg_rtp_path")
if rpg_rtp_path:
env["RPG_RTP_PATH"] = rpg_rtp_path
return env
def get_runner_command(self):
cmd = [self.get_executable()]
if self.runner_config["fullscreen"]:
cmd.append("--fullscreen")
else:
cmd.append("--window")
if not self.runner_config["audio"]:
cmd.append("--disable-audio")
if self.runner_config["mouse"]:
cmd.append("--enable-mouse")
if self.runner_config["touch"]:
cmd.append("--enable-touch")
if self.runner_config["hide_title"]:
cmd.append("--hide-title")
if not self.runner_config["vsync"]:
cmd.append("--no-vsync")
fps_limit = self.runner_config.get("fps_limit")
if fps_limit:
cmd.extend(("--fps-limit", fps_limit))
show_fps = self.runner_config.get("show_fps")
if show_fps != "off":
cmd.append("--show-fps")
if show_fps == "full":
cmd.append("--fps-render-window")
if self.runner_config["test_play"]:
cmd.append("--test-play")
seed = self.runner_config.get("seed")
if seed:
cmd.extend(("--seed", seed))
if not self.runner_config["rtp"]:
cmd.append("--disable-rtp")
return cmd
def get_run_data(self):
cmd = self.get_runner_command()
if self.default_path:
game_path = path.expanduser(self.default_path)
cmd.extend(("--project-path", game_path))
return {"command": cmd, "env": self.get_env()}
def play(self):
if not self.game_path:
return {"error": "CUSTOM", "text": _("No game directory provided")}
if not path.isdir(self.game_path):
return self.directory_not_found(self.game_path)
cmd = self.get_runner_command()
cmd.extend(("--project-path", self.game_path))
encoding = self.game_config.get("encoding")
if encoding:
cmd.extend(("--encoding", encoding))
engine = self.game_config.get("engine")
if engine:
cmd.extend(("--engine", engine))
save_path = self.game_config.get("save_path")
if save_path:
save_path = path.expanduser(save_path)
if not path.isdir(save_path):
return self.directory_not_found(save_path)
cmd.extend(("--save-path", save_path))
record_input = self.game_config.get("record_input")
if record_input:
record_input = path.expanduser(record_input)
cmd.extend(("--record-input", record_input))
replay_input = self.game_config.get("replay_input")
if replay_input:
replay_input = path.expanduser(replay_input)
if not path.isfile(replay_input):
return {"error": "FILE_NOT_FOUND", "file": replay_input}
cmd.extend(("--replay-input", replay_input))
load_game_id = self.game_config.get("load_game_id")
if load_game_id:
cmd.extend(("--load-game-id", str(load_game_id)))
start_map_id = self.game_config.get("start_map_id")
if start_map_id:
cmd.extend(("--start-map-id", str(start_map_id)))
start_position = self.game_config.get("start_position")
if start_position:
cmd.extend(("--start-position", *start_position.split()))
start_party = self.game_config.get("start_party")
if start_party:
cmd.extend(("--start-party", *start_party.split()))
battle_test = self.game_config.get("battle_test")
if battle_test:
cmd.extend(("--battle-test", battle_test))
return {"command": cmd}
@staticmethod
def directory_not_found(directory):
error = _(
"The directory {} could not be found"
).format(directory.replace("&", "&"))
return {"error": "CUSTOM", "text": error}
description
¶
download_url
¶
entry_point_option
¶
game_options
¶
game_path
property
readonly
¶
Return the directory where the game is installed.
human_name
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
directory_not_found(directory)
staticmethod
¶
Source code in lutris/runners/easyrpg.py
@staticmethod
def directory_not_found(directory):
error = _(
"The directory {} could not be found"
).format(directory.replace("&", "&"))
return {"error": "CUSTOM", "text": error}
get_env(self, os_env=False)
¶
Return environment variables used for a game.
Source code in lutris/runners/easyrpg.py
def get_env(self, os_env=False):
env = super().get_env(os_env)
rpg2k_rtp_path = self.runner_config.get("rpg2k_rtp_path")
if rpg2k_rtp_path:
env["RPG2K_RTP_PATH"] = rpg2k_rtp_path
rpg2k3_rtp_path = self.runner_config.get("rpg2k3_rtp_path")
if rpg2k3_rtp_path:
env["RPG2K3_RTP_PATH"] = rpg2k3_rtp_path
rpg_rtp_path = self.runner_config.get("rpg_rtp_path")
if rpg_rtp_path:
env["RPG_RTP_PATH"] = rpg_rtp_path
return env
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/easyrpg.py
def get_run_data(self):
cmd = self.get_runner_command()
if self.default_path:
game_path = path.expanduser(self.default_path)
cmd.extend(("--project-path", game_path))
return {"command": cmd, "env": self.get_env()}
get_runner_command(self)
¶
Source code in lutris/runners/easyrpg.py
def get_runner_command(self):
cmd = [self.get_executable()]
if self.runner_config["fullscreen"]:
cmd.append("--fullscreen")
else:
cmd.append("--window")
if not self.runner_config["audio"]:
cmd.append("--disable-audio")
if self.runner_config["mouse"]:
cmd.append("--enable-mouse")
if self.runner_config["touch"]:
cmd.append("--enable-touch")
if self.runner_config["hide_title"]:
cmd.append("--hide-title")
if not self.runner_config["vsync"]:
cmd.append("--no-vsync")
fps_limit = self.runner_config.get("fps_limit")
if fps_limit:
cmd.extend(("--fps-limit", fps_limit))
show_fps = self.runner_config.get("show_fps")
if show_fps != "off":
cmd.append("--show-fps")
if show_fps == "full":
cmd.append("--fps-render-window")
if self.runner_config["test_play"]:
cmd.append("--test-play")
seed = self.runner_config.get("seed")
if seed:
cmd.extend(("--seed", seed))
if not self.runner_config["rtp"]:
cmd.append("--disable-rtp")
return cmd
play(self)
¶
Source code in lutris/runners/easyrpg.py
def play(self):
if not self.game_path:
return {"error": "CUSTOM", "text": _("No game directory provided")}
if not path.isdir(self.game_path):
return self.directory_not_found(self.game_path)
cmd = self.get_runner_command()
cmd.extend(("--project-path", self.game_path))
encoding = self.game_config.get("encoding")
if encoding:
cmd.extend(("--encoding", encoding))
engine = self.game_config.get("engine")
if engine:
cmd.extend(("--engine", engine))
save_path = self.game_config.get("save_path")
if save_path:
save_path = path.expanduser(save_path)
if not path.isdir(save_path):
return self.directory_not_found(save_path)
cmd.extend(("--save-path", save_path))
record_input = self.game_config.get("record_input")
if record_input:
record_input = path.expanduser(record_input)
cmd.extend(("--record-input", record_input))
replay_input = self.game_config.get("replay_input")
if replay_input:
replay_input = path.expanduser(replay_input)
if not path.isfile(replay_input):
return {"error": "FILE_NOT_FOUND", "file": replay_input}
cmd.extend(("--replay-input", replay_input))
load_game_id = self.game_config.get("load_game_id")
if load_game_id:
cmd.extend(("--load-game-id", str(load_game_id)))
start_map_id = self.game_config.get("start_map_id")
if start_map_id:
cmd.extend(("--start-map-id", str(start_map_id)))
start_position = self.game_config.get("start_position")
if start_position:
cmd.extend(("--start-position", *start_position.split()))
start_party = self.game_config.get("start_party")
if start_party:
cmd.extend(("--start-party", *start_party.split()))
battle_test = self.game_config.get("battle_test")
if battle_test:
cmd.extend(("--battle-test", battle_test))
return {"command": cmd}
fsuae
¶
fsuae (Runner)
¶
Source code in lutris/runners/fsuae.py
class fsuae(Runner):
human_name = _("FS-UAE")
description = _("Amiga emulator")
platforms = [
_("Amiga 500"),
_("Amiga 500+"),
_("Amiga 600"),
_("Amiga 1000"),
_("Amiga 1200"),
_("Amiga 1200"),
_("Amiga 4000"),
_("Amiga CD32"),
_("Commodore CDTV"),
]
model_choices = [
(_("Amiga 500"), "A500"),
(_("Amiga 500+ with 1 MB chip RAM"), "A500+"),
(_("Amiga 600 with 1 MB chip RAM"), "A600"),
(_("Amiga 1000 with 512 KB chip RAM"), "A1000"),
(_("Amiga 1200 with 2 MB chip RAM"), "A1200"),
(_("Amiga 1200 but with 68020 processor"), "A1200/020"),
(_("Amiga 4000 with 2 MB chip RAM and a 68040"), "A4000/040"),
(_("Amiga CD32"), "CD32"),
(_("Commodore CDTV"), "CDTV"),
]
cpumodel_choices = [
(_("68000"), "68000"),
(_("68010"), "68010"),
(_("68020 with 24-bit addressing"), "68EC020"),
(_("68020"), "68020"),
(_("68030 without internal MMU"), "68EC030"),
(_("68030"), "68030"),
(_("68040 without internal FPU and MMU"), "68EC040"),
(_("68040 without internal FPU"), "68LC040"),
(_("68040 without internal MMU"), "68040-NOMMU"),
(_("68040"), "68040"),
(_("68060 without internal FPU and MMU"), "68EC060"),
(_("68060 without internal FPU"), "68LC060"),
(_("68060 without internal MMU"), "68060-NOMMU"),
(_("68060"), "68060"),
(_("Auto"), "auto"),
]
memory_choices = [
(_("0"), "0"),
(_("1 MB"), "1024"),
(_("2 MB"), "2048"),
(_("4 MB"), "4096"),
(_("8 MB"), "8192"),
]
zorroiii_choices = [
(_("0"), "0"),
(_("1 MB"), "1024"),
(_("2 MB"), "2048"),
(_("4 MB"), "4096"),
(_("8 MB"), "8192"),
(_("16 MB"), "16384"),
(_("32 MB"), "32768"),
(_("64 MB"), "65536"),
(_("128 MB"), "131072"),
(_("256 MB"), "262144"),
(_("384 MB"), "393216"),
(_("512 MB"), "524288"),
(_("768 MB"), "786432"),
(_("1 GB"), "1048576"),
]
flsound_choices = [
("0", "0"),
("25", "25"),
("50", "50"),
("75", "75"),
("100", "100"),
]
gpucard_choices = [
("None", "None"),
("UAEGFX", "uaegfx"),
("UAEGFX Zorro II", "uaegfx-z2"),
("UAEGFX Zorro III", "uaegfx-z3"),
("Picasso II Zorro II", "picasso-ii"),
("Picasso II+ Zorro II", "picasso-ii+"),
("Picasso IV", "picasso-iv"),
("Picasso IV Zorro II", "picasso-iv-z2"),
("Picasso IV Zorro III", "picasso-iv-z3"),
]
gpumem_choices = [
(_("0"), "0"),
(_("1 MB"), "1024"),
(_("2 MB"), "2048"),
(_("4 MB"), "4096"),
(_("8 MB"), "8192"),
(_("16 MB"), "16384"),
(_("32 MB"), "32768"),
(_("64 MB"), "65536"),
(_("128 MB"), "131072"),
(_("256 MB"), "262144"),
]
flspeed_choices = [
(_("Turbo"), "0"),
("100%", "100"),
("200%", "200"),
("400%", "400"),
("800%", "800"),
]
runner_executable = "fs-uae/fs-uae"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("Boot disk"),
"default_path": "game_path",
"help": _(
"The main floppy disk file with the game data. \n"
"FS-UAE supports floppy images in multiple file formats: "
"ADF, IPF, DMS are the most common. ADZ (compressed ADF) "
"and ADFs in zip files are a also supported.\n"
"Files ending in .hdf will be mounted as hard drives and "
"ISOs can be used for Amiga CD32 and CDTV models."
),
}, {
"option": "disks",
"type": "multiple",
"label": _("Additionnal floppies"),
"default_path": "game_path",
"help": _("The additional floppy disk image(s)."),
}, {
"option": "cdrom_image",
"label": _("CD-ROM image"),
"type": "file",
"help": _("CD-ROM image to use on non CD32/CDTV models")
}
]
runner_options = [
{
"option": "model",
"label": _("Amiga model"),
"type": "choice",
"choices": model_choices,
"default": "A500",
"help": _("Specify the Amiga model you want to emulate."),
},
{
"option": "kickstart_file",
"label": _("Kickstart ROMs location"),
"type": "file",
"help": _(
"Choose the folder containing original Amiga Kickstart "
"ROMs. Refer to FS-UAE documentation to find how to "
"acquire them. Without these, FS-UAE uses a bundled "
"replacement ROM which is less compatible with Amiga "
"software."
),
},
{
"option": "kickstart_ext_file",
"label": _("Extended Kickstart location"),
"type": "file",
"advanced": True,
"help": _("Location of extended Kickstart used for CD32"),
},
{
"option": "gfx_fullscreen_amiga",
"label": _("Fullscreen (F12 + S to switch)"),
"type": "bool",
"default": False,
},
{
"option": "scanlines",
"label": _("Scanlines display style"),
"type": "bool",
"default": False,
"help": _("Activates a display filter adding scanlines to imitate "
"the displays of yesteryear."),
},
{
"option": "cpumodel",
"label": _("CPU"),
"type": "choice",
"choices": cpumodel_choices,
"default": "auto",
"advanced": True,
"help": _("Use this option to override the CPU model in the emulated Amiga. All Amiga "
"models imply a default CPU model, so you only need to use this option if you "
"want to use another CPU."),
},
{
"option": "fmemory",
"label": _("Fast Memory"),
"type": "choice",
"choices": memory_choices,
"default": "0",
"advanced": True,
"help": _("Specify how much Fast Memory the Amiga model should have."),
},
{
"option": "ziiimem",
"label": _("Zorro III RAM"),
"type": "choice",
"choices": zorroiii_choices,
"default": "0",
"advanced": True,
"help": _("Override the amount of Zorro III Fast memory, specified in KB. Must be a "
"multiple of 1024. The default value depends on [amiga_model]. Requires a "
"processor with 32-bit address bus, (use for example the A1200/020 model)."),
},
{
"option": "fdvolume",
"label": _("Floppy Drive Volume"),
"type": "choice",
"choices": flsound_choices,
"default": "0",
"advanced": True,
"help": _("Set volume to 0 to disable floppy drive clicks "
"when the drive is empty. Max volume is 100.")
},
{
"option": "fdspeed",
"label": _("Floppy Drive Speed"),
"type": "choice",
"choices": flspeed_choices,
"default": "100",
"advanced": True,
"help": _(
"Set the speed of the emulated floppy drives, in percent. "
"For example, you can specify 800 to get an 8x increase in "
"speed. Use 0 to specify turbo mode. Turbo mode means that "
"all floppy operations complete immediately. The default is 100 for most models."
)
},
{
"option": "grafixcard",
"label": _("Graphics Card"),
"type": "choice",
"choices": gpucard_choices,
"default": "None",
"advanced": True,
"help": _(
"Use this option to enable a graphics card. This option is none by default, in "
"which case only chipset graphics (OCS/ECS/AGA) support is available."
)
},
{
"option": "grafixmemory",
"label": _("Graphics Card RAM"),
"type": "choice",
"choices": gpumem_choices,
"default": "0",
"advanced": True,
"help": _(
"Override the amount of graphics memory on the graphics card. The 0 MB option is "
"not really valid, but exists for user interface reasons."
)
},
{
"option": "jitcompiler",
"label": _("JIT Compiler"),
"type": "bool",
"default": False,
"advanced": True,
},
{
"option": "gamemode",
"label": _("Feral GameMode"),
"type": "bool",
"default": False,
"advanced": True,
"help": _("Automatically uses Feral GameMode daemon if available. "
"Set to true to disable the feature.")
},
{
"option": "govwarning",
"label": _("CPU governor warning"),
"type": "bool",
"default": False,
"advanced": True,
"help":
_("Warn if running with a CPU governor other than performance. "
"Set to true to disable the warning.")
},
{
"option": "bsdsocket",
"label": _("UAE bsdsocket.library"),
"type": "bool",
"default": False,
"advanced": True,
},
]
def get_platform(self):
model = self.runner_config.get("model")
if model:
for index, machine in enumerate(self.model_choices):
if machine[1] == model:
return self.platforms[index]
return ""
def get_absolute_path(self, path):
"""Return the absolute path for a file"""
return path if os.path.isabs(path) else os.path.join(self.game_path, path)
def insert_floppies(self):
disks = []
main_disk = self.game_config.get("main_file")
if main_disk:
disks.append(main_disk)
game_disks = self.game_config.get("disks") or []
for disk in game_disks:
if disk not in disks:
disks.append(disk)
# Make all paths absolute
disks = [self.get_absolute_path(disk) for disk in disks]
drives = []
floppy_images = []
for drive, disk_path in enumerate(disks):
disk_param = self.get_disk_param(disk_path)
drives.append("--%s_%d=%s" % (disk_param, drive, disk_path))
if disk_param == "floppy_drive":
floppy_images.append("--floppy_image_%d=%s" % (drive, disk_path))
cdrom_image = self.game_config.get("cdrom_image")
if cdrom_image:
drives.append("--cdrom_drive_0=%s" % self.get_absolute_path(cdrom_image))
return drives + floppy_images
def get_disk_param(self, disk_path):
amiga_model = self.runner_config.get("model")
if amiga_model in ("CD32", "CDTV"):
return "cdrom_drive"
if disk_path.lower().endswith(".hdf"):
return "hard_drive"
return "floppy_drive"
def get_params(self): # pylint: disable=too-many-branches
params = []
option_params = {
"kickstart_file": "--kickstart_file=%s",
"kickstart_ext_file": "--kickstart_ext_file=%s",
"model": "--amiga_model=%s",
"cpumodel": "--cpu=%s",
"fmemory": "--fast_memory=%s",
"ziiimem": "--zorro_iii_memory=%s",
"fdvolume": "--floppy_drive_volume=%s",
"fdspeed": "--floppy_drive_speed=%s",
"grafixcard": "--graphics_card=%s",
"grafixmemory": "--graphics_memory=%s",
}
for option, param in option_params.items():
option_value = self.runner_config.get(option)
if option_value:
params.append(param % option_value)
if self.runner_config.get("gfx_fullscreen_amiga"):
width = int(DISPLAY_MANAGER.get_current_resolution()[0])
params.append("--fullscreen")
# params.append("--fullscreen_mode=fullscreen-window")
params.append("--fullscreen_mode=fullscreen")
params.append("--fullscreen_width=%d" % width)
if self.runner_config.get("jitcompiler"):
params.append("--jit_compiler=1")
if self.runner_config.get("bsdsocket"):
params.append("--bsdsocket_library=1")
if self.runner_config.get("gamemode"):
params.append("--game_mode=0")
if self.runner_config.get("govwarning"):
params.append("--governor_warning=0")
if self.runner_config.get("scanlines"):
params.append("--scanlines=1")
return params
def play(self):
return {"command": [self.get_executable()] + self.get_params() + self.insert_floppies()}
cpumodel_choices
¶
description
¶
flsound_choices
¶
flspeed_choices
¶
game_options
¶
gpucard_choices
¶
gpumem_choices
¶
human_name
¶
memory_choices
¶
model_choices
¶
platforms
¶
runner_executable
¶
runner_options
¶
zorroiii_choices
¶
get_absolute_path(self, path)
¶
Return the absolute path for a file
Source code in lutris/runners/fsuae.py
def get_absolute_path(self, path):
"""Return the absolute path for a file"""
return path if os.path.isabs(path) else os.path.join(self.game_path, path)
get_disk_param(self, disk_path)
¶
Source code in lutris/runners/fsuae.py
def get_disk_param(self, disk_path):
amiga_model = self.runner_config.get("model")
if amiga_model in ("CD32", "CDTV"):
return "cdrom_drive"
if disk_path.lower().endswith(".hdf"):
return "hard_drive"
return "floppy_drive"
get_params(self)
¶
Source code in lutris/runners/fsuae.py
def get_params(self): # pylint: disable=too-many-branches
params = []
option_params = {
"kickstart_file": "--kickstart_file=%s",
"kickstart_ext_file": "--kickstart_ext_file=%s",
"model": "--amiga_model=%s",
"cpumodel": "--cpu=%s",
"fmemory": "--fast_memory=%s",
"ziiimem": "--zorro_iii_memory=%s",
"fdvolume": "--floppy_drive_volume=%s",
"fdspeed": "--floppy_drive_speed=%s",
"grafixcard": "--graphics_card=%s",
"grafixmemory": "--graphics_memory=%s",
}
for option, param in option_params.items():
option_value = self.runner_config.get(option)
if option_value:
params.append(param % option_value)
if self.runner_config.get("gfx_fullscreen_amiga"):
width = int(DISPLAY_MANAGER.get_current_resolution()[0])
params.append("--fullscreen")
# params.append("--fullscreen_mode=fullscreen-window")
params.append("--fullscreen_mode=fullscreen")
params.append("--fullscreen_width=%d" % width)
if self.runner_config.get("jitcompiler"):
params.append("--jit_compiler=1")
if self.runner_config.get("bsdsocket"):
params.append("--bsdsocket_library=1")
if self.runner_config.get("gamemode"):
params.append("--game_mode=0")
if self.runner_config.get("govwarning"):
params.append("--governor_warning=0")
if self.runner_config.get("scanlines"):
params.append("--scanlines=1")
return params
get_platform(self)
¶
Source code in lutris/runners/fsuae.py
def get_platform(self):
model = self.runner_config.get("model")
if model:
for index, machine in enumerate(self.model_choices):
if machine[1] == model:
return self.platforms[index]
return ""
insert_floppies(self)
¶
Source code in lutris/runners/fsuae.py
def insert_floppies(self):
disks = []
main_disk = self.game_config.get("main_file")
if main_disk:
disks.append(main_disk)
game_disks = self.game_config.get("disks") or []
for disk in game_disks:
if disk not in disks:
disks.append(disk)
# Make all paths absolute
disks = [self.get_absolute_path(disk) for disk in disks]
drives = []
floppy_images = []
for drive, disk_path in enumerate(disks):
disk_param = self.get_disk_param(disk_path)
drives.append("--%s_%d=%s" % (disk_param, drive, disk_path))
if disk_param == "floppy_drive":
floppy_images.append("--floppy_image_%d=%s" % (drive, disk_path))
cdrom_image = self.game_config.get("cdrom_image")
if cdrom_image:
drives.append("--cdrom_drive_0=%s" % self.get_absolute_path(cdrom_image))
return drives + floppy_images
play(self)
¶
Source code in lutris/runners/fsuae.py
def play(self):
return {"command": [self.get_executable()] + self.get_params() + self.insert_floppies()}
hatari
¶
hatari (Runner)
¶
Source code in lutris/runners/hatari.py
class hatari(Runner):
human_name = _("Hatari")
description = _("Atari ST computers emulator")
platforms = [_("Atari ST")]
runnable_alone = True
runner_executable = "hatari/bin/hatari"
entry_point_option = "disk-a"
game_options = [
{
"option":
"disk-a",
"type":
"file",
"label":
_("Floppy Disk A"),
"help": _(
"Hatari supports floppy disk images in the following "
"formats: ST, DIM, MSA, STX, IPF, RAW and CRT. The last "
"three require the caps library (capslib). ZIP is "
"supported, you don't need to uncompress the file."
),
},
{
"option":
"disk-b",
"type":
"file",
"label":
_("Floppy Disk B"),
"help": _(
"Hatari supports floppy disk images in the following "
"formats: ST, DIM, MSA, STX, IPF, RAW and CRT. The last "
"three require the caps library (capslib). ZIP is "
"supported, you don't need to uncompress the file."
),
},
]
joystick_choices = [(_("None"), "none"), (_("Keyboard"), "keys"), (_("Joystick"), "real")]
runner_options = [
{
"option":
"bios_file",
"type":
"file",
"label":
_("Bios file (TOS)"),
"help": _(
"TOS is the operating system of the Atari ST "
"and is necessary to run applications with the best "
"fidelity, minimizing risks of issues.\n"
"TOS 1.02 is recommended for games."
),
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "zoom",
"type": "bool",
"label": _("Scale up display by 2 (Atari ST/STE)"),
"default": True,
"help": _("Double the screen size in windowed mode."),
},
{
"option":
"borders",
"type":
"bool",
"label":
_("Add borders to display"),
"default":
False,
"help": _(
"Useful for some games and demos using the overscan "
"technique. The Atari ST displayed borders around the "
"screen because it was not powerful enough to display "
"graphics in fullscreen. But people from the demo scene "
"were able to remove them and some games made use of "
"this technique."
),
},
{
"option":
"status",
"type":
"bool",
"label":
_("Display status bar"),
"default":
False,
"help": _(
"Displays a status bar with some useful information, "
"like green leds lighting up when the floppy disks are "
"read."
),
},
{
"option": "joy0",
"type": "choice",
"label": _("Joystick 1"),
"choices": joystick_choices,
"default": "none",
},
{
"option": "joy1",
"type": "choice",
"label": _("Joystick 2"),
"choices": joystick_choices,
"default": "none",
},
]
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
bios_path = system.create_folder("~/.hatari/bios")
dlg = QuestionDialog(
{
"question": _("Do you want to select an Atari ST BIOS file?"),
"title": _("Use BIOS file?"),
}
)
if dlg.result == dlg.YES:
bios_dlg = FileDialog(_("Select a BIOS file"))
bios_filename = bios_dlg.filename
if not bios_filename:
return
shutil.copy(bios_filename, bios_path)
bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
config = LutrisConfig(runner_slug="hatari")
config.raw_runner_config.update({"bios_file": bios_path})
config.save()
if callback:
callback()
super().install(version=version, downloader=downloader, callback=on_runner_installed)
def play(self): # pylint: disable=too-many-branches
params = [self.get_executable()]
if self.runner_config.get("fullscreen"):
params.append("--fullscreen")
else:
params.append("--window")
params.append("--zoom")
if self.runner_config.get("zoom"):
params.append("2")
else:
params.append("1")
params.append("--borders")
if self.runner_config.get("borders"):
params.append("true")
else:
params.append("false")
params.append("--statusbar")
if self.runner_config.get("status"):
params.append("true")
else:
params.append("false")
if self.runner_config.get("joy0"):
params.append("--joy0")
params.append(self.runner_config["joy0"])
if self.runner_config.get("joy1"):
params.append("--joy1")
params.append(self.runner_config["joy1"])
if system.path_exists(self.runner_config.get("bios_file", "")):
params.append("--tos")
params.append(self.runner_config["bios_file"])
else:
return {"error": "NO_BIOS"}
diska = self.game_config.get("disk-a")
if not system.path_exists(diska):
return {"error": "FILE_NOT_FOUND", "file": diska}
params.append("--disk-a")
params.append(diska)
return {"command": params}
description
¶
entry_point_option
¶
game_options
¶
human_name
¶
joystick_choices
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/hatari.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
bios_path = system.create_folder("~/.hatari/bios")
dlg = QuestionDialog(
{
"question": _("Do you want to select an Atari ST BIOS file?"),
"title": _("Use BIOS file?"),
}
)
if dlg.result == dlg.YES:
bios_dlg = FileDialog(_("Select a BIOS file"))
bios_filename = bios_dlg.filename
if not bios_filename:
return
shutil.copy(bios_filename, bios_path)
bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
config = LutrisConfig(runner_slug="hatari")
config.raw_runner_config.update({"bios_file": bios_path})
config.save()
if callback:
callback()
super().install(version=version, downloader=downloader, callback=on_runner_installed)
play(self)
¶
Source code in lutris/runners/hatari.py
def play(self): # pylint: disable=too-many-branches
params = [self.get_executable()]
if self.runner_config.get("fullscreen"):
params.append("--fullscreen")
else:
params.append("--window")
params.append("--zoom")
if self.runner_config.get("zoom"):
params.append("2")
else:
params.append("1")
params.append("--borders")
if self.runner_config.get("borders"):
params.append("true")
else:
params.append("false")
params.append("--statusbar")
if self.runner_config.get("status"):
params.append("true")
else:
params.append("false")
if self.runner_config.get("joy0"):
params.append("--joy0")
params.append(self.runner_config["joy0"])
if self.runner_config.get("joy1"):
params.append("--joy1")
params.append(self.runner_config["joy1"])
if system.path_exists(self.runner_config.get("bios_file", "")):
params.append("--tos")
params.append(self.runner_config["bios_file"])
else:
return {"error": "NO_BIOS"}
diska = self.game_config.get("disk-a")
if not system.path_exists(diska):
return {"error": "FILE_NOT_FOUND", "file": diska}
params.append("--disk-a")
params.append(diska)
return {"command": params}
json
¶
Base class and utilities for JSON based runners
JSON_RUNNER_DIRS
¶
JsonRunner (Runner)
¶
Source code in lutris/runners/json.py
class JsonRunner(Runner):
json_path = None
def __init__(self, config=None):
super().__init__(config)
if not self.json_path:
raise RuntimeError("Create subclasses of JsonRunner with the json_path attribute set")
with open(self.json_path, encoding='utf-8') as json_file:
self._json_data = json.load(json_file)
self.game_options = self._json_data["game_options"]
self.runner_options = self._json_data.get("runner_options", [])
self.human_name = self._json_data["human_name"]
self.description = self._json_data["description"]
self.platforms = self._json_data["platforms"]
self.runner_executable = self._json_data["runner_executable"]
self.system_options_override = self._json_data.get("system_options_override", [])
self.entry_point_option = self._json_data.get("entry_point_option", "main_file")
self.download_url = self._json_data.get("download_url")
def play(self):
"""Return a launchable command constructed from the options"""
arguments = [self.get_executable()]
for option in self.runner_options:
if option["option"] not in self.runner_config:
continue
if option["type"] == "bool":
if self.runner_config.get(option["option"]):
arguments.append(option["argument"])
elif option["type"] == "choice":
if self.runner_config.get(option["option"]) != "off":
arguments.append(option["argument"])
arguments.append(self.runner_config.get(option["option"]))
else:
raise RuntimeError("Unhandled type %s" % option["type"])
main_file = self.game_config.get(self.entry_point_option)
if not system.path_exists(main_file):
return {"error": "FILE_NOT_FOUND", "file": main_file}
arguments.append(main_file)
return {"command": arguments}
json_path
¶
__init__(self, config=None)
special
¶
Source code in lutris/runners/json.py
def __init__(self, config=None):
super().__init__(config)
if not self.json_path:
raise RuntimeError("Create subclasses of JsonRunner with the json_path attribute set")
with open(self.json_path, encoding='utf-8') as json_file:
self._json_data = json.load(json_file)
self.game_options = self._json_data["game_options"]
self.runner_options = self._json_data.get("runner_options", [])
self.human_name = self._json_data["human_name"]
self.description = self._json_data["description"]
self.platforms = self._json_data["platforms"]
self.runner_executable = self._json_data["runner_executable"]
self.system_options_override = self._json_data.get("system_options_override", [])
self.entry_point_option = self._json_data.get("entry_point_option", "main_file")
self.download_url = self._json_data.get("download_url")
play(self)
¶
Return a launchable command constructed from the options
Source code in lutris/runners/json.py
def play(self):
"""Return a launchable command constructed from the options"""
arguments = [self.get_executable()]
for option in self.runner_options:
if option["option"] not in self.runner_config:
continue
if option["type"] == "bool":
if self.runner_config.get(option["option"]):
arguments.append(option["argument"])
elif option["type"] == "choice":
if self.runner_config.get(option["option"]) != "off":
arguments.append(option["argument"])
arguments.append(self.runner_config.get(option["option"]))
else:
raise RuntimeError("Unhandled type %s" % option["type"])
main_file = self.game_config.get(self.entry_point_option)
if not system.path_exists(main_file):
return {"error": "FILE_NOT_FOUND", "file": main_file}
arguments.append(main_file)
return {"command": arguments}
load_json_runners()
¶
Source code in lutris/runners/json.py
def load_json_runners():
json_runners = {}
for json_dir in JSON_RUNNER_DIRS:
if not os.path.exists(json_dir):
continue
for json_path in os.listdir(json_dir):
if not json_path.endswith(".json"):
continue
runner_name = json_path[:-5]
runner_class = type(
runner_name,
(JsonRunner, ),
{'json_path': os.path.join(json_dir, json_path)}
)
json_runners[runner_name] = runner_class
return json_runners
jzintv
¶
jzintv (Runner)
¶
Source code in lutris/runners/jzintv.py
class jzintv(Runner):
human_name = _("jzIntv")
description = _("Intellivision Emulator")
platforms = [_("Intellivision")]
runner_executable = "jzintv/bin/jzintv"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"default_path": "game_path",
"help": _(
"The game data, commonly called a ROM image. \n"
"Supported formats: ROM, BIN+CFG, INT, ITV \n"
"The file extension must be lower-case."
),
}
]
runner_options = [
{
"option": "bios_path",
"type": "directory_chooser",
"label": _("Bios location"),
"help": _(
"Choose the folder containing the Intellivision BIOS "
"files (exec.bin and grom.bin).\n"
"These files contain code from the original hardware "
"necessary to the emulation."
),
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen")
},
{
"option": "resolution",
"type": "choice",
"label": _("Resolution"),
"choices": (
("320 x 200", "0"),
("640 x 480", "1"),
("800 x 400", "5"),
("800 x 600", "2"),
("1024 x 768", "3"),
("1680 x 1050", "4"),
("1600 x 1200", "6"),
),
"default": "0"
},
]
def play(self):
"""Run Intellivision game"""
arguments = [self.get_executable()]
selected_resolution = self.runner_config.get("resolution")
if selected_resolution:
arguments = arguments + ["-z%s" % selected_resolution]
if self.runner_config.get("fullscreen"):
arguments = arguments + ["-f"]
bios_path = self.runner_config.get("bios_path", "")
if system.path_exists(bios_path):
arguments.append("--execimg=%s/exec.bin" % bios_path)
arguments.append("--gromimg=%s/grom.bin" % bios_path)
else:
return {"error": "NO_BIOS"}
rom_path = self.game_config.get("main_file") or ""
if not system.path_exists(rom_path):
return {"error": "FILE_NOT_FOUND", "file": rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)
arguments += ["--rom-path=%s/" % romdir]
arguments += [romfile]
return {"command": arguments}
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
play(self)
¶
Run Intellivision game
Source code in lutris/runners/jzintv.py
def play(self):
"""Run Intellivision game"""
arguments = [self.get_executable()]
selected_resolution = self.runner_config.get("resolution")
if selected_resolution:
arguments = arguments + ["-z%s" % selected_resolution]
if self.runner_config.get("fullscreen"):
arguments = arguments + ["-f"]
bios_path = self.runner_config.get("bios_path", "")
if system.path_exists(bios_path):
arguments.append("--execimg=%s/exec.bin" % bios_path)
arguments.append("--gromimg=%s/grom.bin" % bios_path)
else:
return {"error": "NO_BIOS"}
rom_path = self.game_config.get("main_file") or ""
if not system.path_exists(rom_path):
return {"error": "FILE_NOT_FOUND", "file": rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)
arguments += ["--rom-path=%s/" % romdir]
arguments += [romfile]
return {"command": arguments}
libretro
¶
libretro runner
LIBRETRO_CORES
¶
libretro (Runner)
¶
Source code in lutris/runners/libretro.py
class libretro(Runner):
human_name = _("Libretro")
description = _("Multi-system emulator")
runnable_alone = True
runner_executable = "retroarch/retroarch"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file")
},
{
"option": "core",
"type": "choice",
"label": _("Core"),
"choices": get_core_choices(),
},
]
runner_options = [
{
"option": "config_file",
"type": "file",
"label": _("Config file"),
"default": get_default_config_path("retroarch.cfg"),
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": True,
},
{
"option": "verbose",
"type": "bool",
"label": _("Verbose logging"),
"default": False,
},
]
@property
def platforms(self):
return [core[2] for core in LIBRETRO_CORES]
def get_platform(self):
game_core = self.game_config.get("core")
if not game_core:
logger.warning("Game don't have a core set")
return
for core in LIBRETRO_CORES:
if core[1] == game_core:
return core[2]
logger.warning("'%s' not found in Libretro cores", game_core)
return ""
def get_core_path(self, core):
"""Return the path of a core, prioritizing Retroarch cores"""
lutris_cores_folder = os.path.join(settings.RUNNER_DIR, "retroarch", "cores")
retroarch_core_folder = os.path.join(os.path.expanduser("~/.config/retroarch/cores"))
core_filename = "{}_libretro.so".format(core)
retroarch_core = os.path.join(retroarch_core_folder, core_filename)
if system.path_exists(retroarch_core):
return retroarch_core
return os.path.join(lutris_cores_folder, core_filename)
def get_version(self, use_default=True):
return self.game_config["core"]
def is_retroarch_installed(self):
return system.path_exists(self.get_executable())
def is_installed(self, core=None):
if not core and self.game_config.get("core"):
core = self.game_config["core"]
if not core or self.runner_config.get("runner_executable"):
return self.is_retroarch_installed()
is_core_installed = system.path_exists(self.get_core_path(core))
return self.is_retroarch_installed() and is_core_installed
def install(self, version=None, downloader=None, callback=None):
captured_super = super() # super() does not work inside install_core()
def install_core():
if not version:
if callback:
callback()
else:
captured_super.install(version, downloader, callback)
if not self.is_retroarch_installed():
captured_super.install(version=None, downloader=downloader, callback=install_core)
else:
captured_super.install(version, downloader, callback)
def get_run_data(self):
return {
"command": [self.get_executable()] + self.get_runner_parameters(),
"env": self.get_env(),
}
def get_config_file(self):
return self.runner_config.get("config_file") or get_default_config_path("retroarch.cfg")
@staticmethod
def get_system_directory(retro_config):
"""Return the system directory used for storing BIOS and firmwares."""
system_directory = retro_config["system_directory"]
if not system_directory or system_directory == "default":
system_directory = get_default_config_path("system")
return os.path.expanduser(system_directory)
def prelaunch(self):
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
config_file = self.get_config_file()
# TODO: review later
# Create retroarch.cfg if it doesn't exist.
if not system.path_exists(config_file):
with open(config_file, "w", encoding='utf-8') as f:
f.write("# Lutris RetroArch Configuration")
f.close()
# Build the default config settings.
retro_config = RetroConfig(config_file)
retro_config["libretro_directory"] = get_default_config_path("cores")
retro_config["libretro_info_path"] = get_default_config_path("info")
retro_config["content_database_path"] = get_default_config_path("database/rdb")
retro_config["cheat_database_path"] = get_default_config_path("database/cht")
retro_config["cursor_directory"] = get_default_config_path("database/cursors")
retro_config["screenshot_directory"] = get_default_config_path("screenshots")
retro_config["input_remapping_directory"] = get_default_config_path("remaps")
retro_config["video_shader_dir"] = get_default_config_path("shaders")
retro_config["core_assets_directory"] = get_default_config_path("downloads")
retro_config["thumbnails_directory"] = get_default_config_path("thumbnails")
retro_config["playlist_directory"] = get_default_config_path("playlists")
retro_config["joypad_autoconfig_dir"] = get_default_config_path("autoconfig")
retro_config["rgui_config_directory"] = get_default_config_path("config")
retro_config["overlay_directory"] = get_default_config_path("overlay")
retro_config["assets_directory"] = get_default_config_path("assets")
retro_config.save()
else:
retro_config = RetroConfig(config_file)
core = self.game_config.get("core")
info_file = os.path.join(get_default_config_path("info"), "{}_libretro.info".format(core))
if system.path_exists(info_file):
retro_config = RetroConfig(info_file)
try:
firmware_count = int(retro_config["firmware_count"])
except (ValueError, TypeError):
firmware_count = 0
system_path = self.get_system_directory(retro_config)
notes = str(retro_config["notes"] or "")
checksums = {}
if notes.startswith("Suggested md5sums:"):
parts = notes.split("|")
for part in parts[1:]:
checksum, filename = part.split(" = ")
checksums[filename] = checksum
for index in range(firmware_count):
firmware_filename = retro_config["firmware%d_path" % index]
firmware_path = os.path.join(system_path, firmware_filename)
if system.path_exists(firmware_path):
if firmware_filename in checksums:
checksum = system.get_md5_hash(firmware_path)
if checksum == checksums[firmware_filename]:
checksum_status = "Checksum good"
else:
checksum_status = "Checksum failed"
else:
checksum_status = "No checksum info"
logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
else:
logger.warning("Firmware '%s' not found!", firmware_filename)
# Before closing issue #431
# TODO check for firmware*_opt and display an error message if
# firmware is missing
# TODO Add dialog for copying the firmware in the correct
# location
return True
def get_runner_parameters(self):
parameters = []
# Fullscreen
fullscreen = self.runner_config.get("fullscreen")
if fullscreen:
parameters.append("--fullscreen")
# Verbose
verbose = self.runner_config.get("verbose")
if verbose:
parameters.append("--verbose")
parameters.append("--config={}".format(self.get_config_file()))
return parameters
def play(self):
command = [self.get_executable()]
command += self.get_runner_parameters()
# Core
core = self.game_config.get("core")
if not core:
return {
"error": "CUSTOM",
"text": _("No core has been selected for this game"),
}
command.append("--libretro={}".format(self.get_core_path(core)))
# Ensure the core is available
if not self.is_installed(core):
self.install(core)
# Main file
file = self.game_config.get("main_file")
if not file:
return {"error": "CUSTOM", "text": _("No game file specified")}
if not system.path_exists(file):
return {"error": "FILE_NOT_FOUND", "file": file}
command.append(file)
return {"command": command}
# Checks whether the retroarch or libretro directories can be uninstalled.
def can_uninstall(self):
retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
return os.path.isdir(retroarch_path) or super().can_uninstall()
# Remove the `retroarch` directory.
def uninstall(self):
retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
if os.path.isdir(retroarch_path):
system.remove_folder(retroarch_path)
super().uninstall()
description
¶
game_options
¶
human_name
¶
platforms
property
readonly
¶
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
runnable_alone
¶
runner_executable
¶
runner_options
¶
can_uninstall(self)
¶
Source code in lutris/runners/libretro.py
def can_uninstall(self):
retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
return os.path.isdir(retroarch_path) or super().can_uninstall()
get_config_file(self)
¶
Source code in lutris/runners/libretro.py
def get_config_file(self):
return self.runner_config.get("config_file") or get_default_config_path("retroarch.cfg")
get_core_path(self, core)
¶
Return the path of a core, prioritizing Retroarch cores
Source code in lutris/runners/libretro.py
def get_core_path(self, core):
"""Return the path of a core, prioritizing Retroarch cores"""
lutris_cores_folder = os.path.join(settings.RUNNER_DIR, "retroarch", "cores")
retroarch_core_folder = os.path.join(os.path.expanduser("~/.config/retroarch/cores"))
core_filename = "{}_libretro.so".format(core)
retroarch_core = os.path.join(retroarch_core_folder, core_filename)
if system.path_exists(retroarch_core):
return retroarch_core
return os.path.join(lutris_cores_folder, core_filename)
get_platform(self)
¶
Source code in lutris/runners/libretro.py
def get_platform(self):
game_core = self.game_config.get("core")
if not game_core:
logger.warning("Game don't have a core set")
return
for core in LIBRETRO_CORES:
if core[1] == game_core:
return core[2]
logger.warning("'%s' not found in Libretro cores", game_core)
return ""
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/libretro.py
def get_run_data(self):
return {
"command": [self.get_executable()] + self.get_runner_parameters(),
"env": self.get_env(),
}
get_runner_parameters(self)
¶
Source code in lutris/runners/libretro.py
def get_runner_parameters(self):
parameters = []
# Fullscreen
fullscreen = self.runner_config.get("fullscreen")
if fullscreen:
parameters.append("--fullscreen")
# Verbose
verbose = self.runner_config.get("verbose")
if verbose:
parameters.append("--verbose")
parameters.append("--config={}".format(self.get_config_file()))
return parameters
get_system_directory(retro_config)
staticmethod
¶
Return the system directory used for storing BIOS and firmwares.
Source code in lutris/runners/libretro.py
@staticmethod
def get_system_directory(retro_config):
"""Return the system directory used for storing BIOS and firmwares."""
system_directory = retro_config["system_directory"]
if not system_directory or system_directory == "default":
system_directory = get_default_config_path("system")
return os.path.expanduser(system_directory)
get_version(self, use_default=True)
¶
Source code in lutris/runners/libretro.py
def get_version(self, use_default=True):
return self.game_config["core"]
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/libretro.py
def install(self, version=None, downloader=None, callback=None):
captured_super = super() # super() does not work inside install_core()
def install_core():
if not version:
if callback:
callback()
else:
captured_super.install(version, downloader, callback)
if not self.is_retroarch_installed():
captured_super.install(version=None, downloader=downloader, callback=install_core)
else:
captured_super.install(version, downloader, callback)
is_installed(self, core=None)
¶
Return whether the runner is installed
Source code in lutris/runners/libretro.py
def is_installed(self, core=None):
if not core and self.game_config.get("core"):
core = self.game_config["core"]
if not core or self.runner_config.get("runner_executable"):
return self.is_retroarch_installed()
is_core_installed = system.path_exists(self.get_core_path(core))
return self.is_retroarch_installed() and is_core_installed
is_retroarch_installed(self)
¶
Source code in lutris/runners/libretro.py
def is_retroarch_installed(self):
return system.path_exists(self.get_executable())
play(self)
¶
Source code in lutris/runners/libretro.py
def play(self):
command = [self.get_executable()]
command += self.get_runner_parameters()
# Core
core = self.game_config.get("core")
if not core:
return {
"error": "CUSTOM",
"text": _("No core has been selected for this game"),
}
command.append("--libretro={}".format(self.get_core_path(core)))
# Ensure the core is available
if not self.is_installed(core):
self.install(core)
# Main file
file = self.game_config.get("main_file")
if not file:
return {"error": "CUSTOM", "text": _("No game file specified")}
if not system.path_exists(file):
return {"error": "FILE_NOT_FOUND", "file": file}
command.append(file)
return {"command": command}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/libretro.py
def prelaunch(self):
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
config_file = self.get_config_file()
# TODO: review later
# Create retroarch.cfg if it doesn't exist.
if not system.path_exists(config_file):
with open(config_file, "w", encoding='utf-8') as f:
f.write("# Lutris RetroArch Configuration")
f.close()
# Build the default config settings.
retro_config = RetroConfig(config_file)
retro_config["libretro_directory"] = get_default_config_path("cores")
retro_config["libretro_info_path"] = get_default_config_path("info")
retro_config["content_database_path"] = get_default_config_path("database/rdb")
retro_config["cheat_database_path"] = get_default_config_path("database/cht")
retro_config["cursor_directory"] = get_default_config_path("database/cursors")
retro_config["screenshot_directory"] = get_default_config_path("screenshots")
retro_config["input_remapping_directory"] = get_default_config_path("remaps")
retro_config["video_shader_dir"] = get_default_config_path("shaders")
retro_config["core_assets_directory"] = get_default_config_path("downloads")
retro_config["thumbnails_directory"] = get_default_config_path("thumbnails")
retro_config["playlist_directory"] = get_default_config_path("playlists")
retro_config["joypad_autoconfig_dir"] = get_default_config_path("autoconfig")
retro_config["rgui_config_directory"] = get_default_config_path("config")
retro_config["overlay_directory"] = get_default_config_path("overlay")
retro_config["assets_directory"] = get_default_config_path("assets")
retro_config.save()
else:
retro_config = RetroConfig(config_file)
core = self.game_config.get("core")
info_file = os.path.join(get_default_config_path("info"), "{}_libretro.info".format(core))
if system.path_exists(info_file):
retro_config = RetroConfig(info_file)
try:
firmware_count = int(retro_config["firmware_count"])
except (ValueError, TypeError):
firmware_count = 0
system_path = self.get_system_directory(retro_config)
notes = str(retro_config["notes"] or "")
checksums = {}
if notes.startswith("Suggested md5sums:"):
parts = notes.split("|")
for part in parts[1:]:
checksum, filename = part.split(" = ")
checksums[filename] = checksum
for index in range(firmware_count):
firmware_filename = retro_config["firmware%d_path" % index]
firmware_path = os.path.join(system_path, firmware_filename)
if system.path_exists(firmware_path):
if firmware_filename in checksums:
checksum = system.get_md5_hash(firmware_path)
if checksum == checksums[firmware_filename]:
checksum_status = "Checksum good"
else:
checksum_status = "Checksum failed"
else:
checksum_status = "No checksum info"
logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
else:
logger.warning("Firmware '%s' not found!", firmware_filename)
# Before closing issue #431
# TODO check for firmware*_opt and display an error message if
# firmware is missing
# TODO Add dialog for copying the firmware in the correct
# location
return True
uninstall(self)
¶
Source code in lutris/runners/libretro.py
def uninstall(self):
retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
if os.path.isdir(retroarch_path):
system.remove_folder(retroarch_path)
super().uninstall()
get_core_choices()
¶
Source code in lutris/runners/libretro.py
def get_core_choices():
return [(core[0], core[1]) for core in LIBRETRO_CORES]
get_default_config_path(path='')
¶
Source code in lutris/runners/libretro.py
def get_default_config_path(path=""):
return os.path.join(settings.RUNNER_DIR, "retroarch", path)
get_libretro_cores()
¶
Source code in lutris/runners/libretro.py
def get_libretro_cores():
cores = []
runner_path = get_default_config_path()
if not os.path.exists(runner_path):
logger.warning("No folder at %s", runner_path)
return []
# Get core identifiers from info dir
info_path = get_default_config_path("info")
if not os.path.exists(info_path):
req = requests.get("http://buildbot.libretro.com/assets/frontend/info.zip", allow_redirects=True)
if req.status_code == requests.codes.ok: # pylint: disable=no-member
with open(get_default_config_path('info.zip'), 'wb') as info_zip:
info_zip.write(req.content)
with ZipFile(get_default_config_path('info.zip'), 'r') as info_zip:
info_zip.extractall(info_path)
else:
logger.error("Error retrieving libretro info archive from server: %s - %s", req.status_code, req.reason)
return []
# Parse info files to fetch display name and platform/system
for info_file in os.listdir(info_path):
if "_libretro.info" not in info_file:
continue
core_identifier = info_file.replace("_libretro.info", "")
core_config = RetroConfig(os.path.join(info_path, info_file))
if "categories" in core_config.keys() and "Emulator" in core_config["categories"]:
core_label = core_config["display_name"] or ""
core_system = core_config["systemname"] or ""
cores.append((core_label, core_identifier, core_system))
cores.sort(key=itemgetter(0))
return cores
linux
¶
Runner for Linux games
linux (Runner)
¶
Source code in lutris/runners/linux.py
class linux(Runner):
human_name = _("Linux")
description = _("Runs native games")
platforms = [_("Linux")]
entry_point_option = "exe"
game_options = [
{
"option": "exe",
"type": "file",
"default_path": "game_path",
"label": _("Executable"),
"help": _("The game's main executable file"),
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Command line arguments used when launching the game"),
},
{
"option":
"working_dir",
"type":
"directory_chooser",
"label":
_("Working directory"),
"help": _(
"The location where the game is run from.\n"
"By default, Lutris uses the directory of the "
"executable."
),
},
{
"option": "ld_preload",
"type": "file",
"label": _("Preload library"),
"advanced": True,
"help": _("A library to load before running the game's executable."),
},
{
"option":
"ld_library_path",
"type":
"directory_chooser",
"label":
_("Add directory to LD_LIBRARY_PATH"),
"advanced":
True,
"help": _(
"A directory where libraries should be searched for "
"first, before the standard set of directories; this is "
"useful when debugging a new library or using a "
"nonstandard library for special purposes."
),
},
]
def __init__(self, config=None):
super().__init__(config)
self.ld_preload = None
@property
def game_exe(self):
"""Return the game's executable's path. The file may not exist, but
this returns None if the exe path is not defined."""
exe = self.game_config.get("exe")
if not exe:
return None
if os.path.isabs(exe):
return exe
if self.game_path:
return os.path.join(self.game_path, exe)
return system.find_executable(exe)
def get_relative_exe(self):
"""Return a relative path if a working dir is set in the options
Some games such as Unreal Gold fail to run if given the absolute path
"""
exe_path = self.game_exe
working_dir = self.game_config.get("working_dir")
if exe_path and working_dir:
parts = exe_path.split(os.path.expanduser(working_dir))
if len(parts) == 2:
return "." + parts[1]
return exe_path
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
option = self.game_config.get("working_dir")
if option:
return os.path.expanduser(option)
if self.game_exe:
return os.path.dirname(self.game_exe)
return super().working_dir
@property
def nvidia_shader_cache_path(self):
"""Linux programs should get individual shader caches if possible."""
return self.game_path or self.shader_cache_dir
def is_installed(self):
"""Well of course Linux is installed, you're using Linux right ?"""
return True
def play(self):
"""Run native game."""
launch_info = {}
if not self.game_exe or not system.path_exists(self.game_exe):
return {"error": "FILE_NOT_FOUND", "file": self.game_exe}
# Quit if the file is not executable
mode = os.stat(self.game_exe).st_mode
if not mode & stat.S_IXUSR:
return {"error": "NOT_EXECUTABLE", "file": self.game_exe}
if not system.path_exists(self.game_exe):
return {"error": "FILE_NOT_FOUND", "file": self.game_exe}
ld_preload = self.game_config.get("ld_preload")
if ld_preload:
launch_info["ld_preload"] = ld_preload
ld_library_path = self.game_config.get("ld_library_path")
if ld_library_path:
launch_info["ld_library_path"] = os.path.expanduser(ld_library_path)
command = [self.get_relative_exe()]
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
launch_info["command"] = command
return launch_info
description
¶
entry_point_option
¶
game_exe
property
readonly
¶
Return the game's executable's path. The file may not exist, but this returns None if the exe path is not defined.
game_options
¶
human_name
¶
nvidia_shader_cache_path
property
readonly
¶
Linux programs should get individual shader caches if possible.
platforms
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
__init__(self, config=None)
special
¶
Source code in lutris/runners/linux.py
def __init__(self, config=None):
super().__init__(config)
self.ld_preload = None
get_relative_exe(self)
¶
Return a relative path if a working dir is set in the options Some games such as Unreal Gold fail to run if given the absolute path
Source code in lutris/runners/linux.py
def get_relative_exe(self):
"""Return a relative path if a working dir is set in the options
Some games such as Unreal Gold fail to run if given the absolute path
"""
exe_path = self.game_exe
working_dir = self.game_config.get("working_dir")
if exe_path and working_dir:
parts = exe_path.split(os.path.expanduser(working_dir))
if len(parts) == 2:
return "." + parts[1]
return exe_path
is_installed(self)
¶
Well of course Linux is installed, you're using Linux right ?
Source code in lutris/runners/linux.py
def is_installed(self):
"""Well of course Linux is installed, you're using Linux right ?"""
return True
play(self)
¶
Run native game.
Source code in lutris/runners/linux.py
def play(self):
"""Run native game."""
launch_info = {}
if not self.game_exe or not system.path_exists(self.game_exe):
return {"error": "FILE_NOT_FOUND", "file": self.game_exe}
# Quit if the file is not executable
mode = os.stat(self.game_exe).st_mode
if not mode & stat.S_IXUSR:
return {"error": "NOT_EXECUTABLE", "file": self.game_exe}
if not system.path_exists(self.game_exe):
return {"error": "FILE_NOT_FOUND", "file": self.game_exe}
ld_preload = self.game_config.get("ld_preload")
if ld_preload:
launch_info["ld_preload"] = ld_preload
ld_library_path = self.game_config.get("ld_library_path")
if ld_library_path:
launch_info["ld_library_path"] = os.path.expanduser(ld_library_path)
command = [self.get_relative_exe()]
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
launch_info["command"] = command
return launch_info
mame
¶
Runner for MAME
MAME_CACHE_DIR
¶
MAME_XML_PATH
¶
mame (Runner)
¶
MAME runner
Source code in lutris/runners/mame.py
class mame(Runner): # pylint: disable=invalid-name
"""MAME runner"""
human_name = _("MAME")
description = _("Arcade game emulator")
runner_executable = "mame/mame"
runnable_alone = True
config_dir = os.path.expanduser("~/.mame")
cache_dir = os.path.join(settings.CACHE_DIR, "mame")
xml_path = os.path.join(cache_dir, "mame.xml")
_platforms = []
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
},
{
"option": "machine",
"type": "choice_with_search",
"label": _("Machine"),
"choices": get_system_choices,
"help": _("The emulated machine.")
},
{
"option": "device",
"type": "choice_with_entry",
"label": _("Storage type"),
"choices": [
(_("Floppy disk"), "flop"),
(_("Floppy drive 1"), "flop1"),
(_("Floppy drive 2"), "flop2"),
(_("Floppy drive 3"), "flop3"),
(_("Floppy drive 4"), "flop4"),
(_("Cassette (tape)"), "cass"),
(_("Cassette 1 (tape)"), "cass1"),
(_("Cassette 2 (tape)"), "cass2"),
(_("Cartridge"), "cart"),
(_("Cartridge 1"), "cart1"),
(_("Cartridge 2"), "cart2"),
(_("Cartridge 3"), "cart3"),
(_("Cartridge 4"), "cart4"),
(_("Snapshot"), "snapshot"),
(_("Hard Disk"), "hard"),
(_("Hard Disk 1"), "hard1"),
(_("Hard Disk 2"), "hard2"),
(_("CD-ROM"), "cdrm"),
(_("CD-ROM 1"), "cdrm1"),
(_("CD-ROM 2"), "cdrm2"),
(_("Snapshot"), "dump"),
(_("Quickload"), "quickload"),
(_("Memory Card"), "memc"),
(_("Cylinder"), "cyln"),
(_("Punch Tape 1"), "ptap1"),
(_("Punch Tape 2"), "ptap2"),
(_("Print Out"), "prin"),
],
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Command line arguments used when launching the game"),
},
{
"option": "autoboot_command",
"type": "string",
"label": _("Autoboot command"),
"help": _("Autotype this command when the system has started, "
"an enter keypress is automatically added."),
},
{
"option": "autoboot_delay",
"type": "range",
"label": _("Delay before entering autoboot command"),
"min": 0,
"max": 120,
}
]
runner_options = [
{
"option": "rompath",
"type": "directory_chooser",
"label": _("ROM/BIOS path"),
"help": _(
"Choose the folder containing ROMs and BIOS files.\n"
"These files contain code from the original hardware "
"necessary to the emulation."
),
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": True,
},
{
"option": "crt",
"type": "bool",
"label": _("CRT effect ()"),
"help": _("Applies a CRT effect to the screen."
"Requires OpenGL renderer."),
"default": False,
},
{
"option": "video",
"type": "choice",
"label": _("Video backend"),
"choices": (
(_("Auto"), ""),
("OpenGL", "opengl"),
("BGFX", "bgfx"),
("SDL2", "accel"),
(_("Software"), "soft"),
),
"default": "opengl",
},
{
"option": "waitvsync",
"type": "bool",
"label": _("Wait for VSync"),
"help":
_("Enable waiting for the start of vblank before "
"flipping screens; reduces tearing effects."),
"advanced": True,
"default": False,
},
{
"option": "uimodekey",
"type": "choice_with_entry",
"label": _("Menu mode key"),
"choices": [
(_("Scroll Lock"), "SCRLOCK"),
(_("Num Lock"), "NUMLOCK"),
(_("Caps Lock"), "CAPSLOCK"),
(_("Menu"), "MENU"),
(_("Right Control"), "RCONTROL"),
(_("Left Control"), "LCONTROL"),
(_("Right Alt"), "RALT"),
(_("Left Alt"), "LALT"),
(_("Right Super"), "RWIN"),
(_("Left Super"), "LWIN"),
],
"default": "SCRLOCK",
"advanced": True,
"help": _("Key to switch between Full Keyboard Mode and "
"Partial Keyboard Mode (default: Scroll Lock)"),
},
]
@property
def working_dir(self):
return os.path.join(settings.RUNNER_DIR, "mame")
@property
def platforms(self):
if self._platforms:
return self.platforms
self._platforms = [choice[0] for choice in get_system_choices(include_year=False)]
self._platforms += [_("Arcade"), _("Nintendo Game & Watch")]
return self._platforms
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
AsyncCall(write_mame_xml, notify_mame_xml)
super().install(version=version, downloader=downloader, callback=on_runner_installed)
@property
def default_path(self):
"""Return the default path, use the runner's rompath"""
main_file = self.game_config.get("main_file")
if main_file:
return os.path.dirname(main_file)
return self.runner_config.get("rompath")
def write_xml_list(self):
"""Write the full game list in XML to disk"""
os.makedirs(self.cache_dir, exist_ok=True)
output = system.execute(
[self.get_executable(), "-listxml"],
env=runtime.get_env()
)
if output:
with open(self.xml_path, "w", encoding='utf-8') as xml_file:
xml_file.write(output)
logger.info("MAME XML list written to %s", self.xml_path)
else:
logger.warning("Couldn't get any output for mame -listxml")
def get_platform(self):
selected_platform = self.game_config.get("platform")
if selected_platform:
return self.platforms[int(selected_platform)]
if self.game_config.get("machine"):
machine_mapping = {choice[1]: choice[0] for choice in get_system_choices(include_year=False)}
return machine_mapping[self.game_config["machine"]]
rom_file = os.path.basename(self.game_config.get("main_file", ""))
if rom_file.startswith("gnw_"):
return _("Nintendo Game & Watch")
return _("Arcade")
def prelaunch(self):
if not system.path_exists(os.path.join(self.config_dir, "mame.ini")):
try:
os.makedirs(self.config_dir)
except OSError:
pass
system.execute(
[self.get_executable(), "-createconfig", "-inipath", self.config_dir],
env=runtime.get_env(),
cwd=self.working_dir
)
return True
def get_shader_params(self, shader_dir, shaders):
"""Returns a list of CLI parameters to apply a list of shaders"""
params = []
shader_path = os.path.join(self.working_dir, "shaders", shader_dir)
for index, shader in enumerate(shaders):
params += [
"-gl_glsl",
"-glsl_shader_mame%s" % index,
os.path.join(shader_path, shader)
]
return params
def play(self):
command = [self.get_executable(), "-skip_gameinfo", "-inipath", self.config_dir]
if self.runner_config.get("video"):
command += ["-video", self.runner_config["video"]]
if not self.runner_config.get("fullscreen"):
command.append("-window")
if self.runner_config.get("waitvsync"):
command.append("-waitvsync")
if self.runner_config.get("uimodekey"):
command += ["-uimodekey", self.runner_config["uimodekey"]]
if self.runner_config.get("crt"):
command += self.get_shader_params("CRT-geom", ["Gaussx", "Gaussy", "CRT-geom-halation"])
command += ["-nounevenstretch"]
if self.game_config.get("machine"):
rompath = self.runner_config.get("rompath")
if rompath:
command += ["-rompath", rompath]
command.append(self.game_config["machine"])
device = self.game_config.get("device")
if not device:
return {'error': "CUSTOM", "text": "No device is set for machine %s" % self.game_config["machine"]}
rom = self.game_config.get("main_file")
if rom:
command += ["-" + device, rom]
else:
rompath = os.path.dirname(self.game_config.get("main_file"))
if not rompath:
rompath = self.runner_config.get("rompath")
rom = os.path.basename(self.game_config.get("main_file"))
if not rompath:
return {'error': 'PATH_NOT_SET', 'path': 'rompath'}
command += ["-rompath", rompath, rom]
if self.game_config.get("autoboot_command"):
command += ["-autoboot_command", self.game_config["autoboot_command"] + "\\n"]
if self.game_config.get("autoboot_delay"):
command += ["-autoboot_delay", str(self.game_config["autoboot_delay"])]
for arg in split_arguments(self.game_config.get("args")):
command.append(arg)
return {"command": command}
cache_dir
¶
config_dir
¶
default_path
property
readonly
¶
Return the default path, use the runner's rompath
description
¶
game_options
¶
human_name
¶
platforms
property
readonly
¶
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
runnable_alone
¶
runner_executable
¶
runner_options
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
xml_path
¶
get_platform(self)
¶
Source code in lutris/runners/mame.py
def get_platform(self):
selected_platform = self.game_config.get("platform")
if selected_platform:
return self.platforms[int(selected_platform)]
if self.game_config.get("machine"):
machine_mapping = {choice[1]: choice[0] for choice in get_system_choices(include_year=False)}
return machine_mapping[self.game_config["machine"]]
rom_file = os.path.basename(self.game_config.get("main_file", ""))
if rom_file.startswith("gnw_"):
return _("Nintendo Game & Watch")
return _("Arcade")
get_shader_params(self, shader_dir, shaders)
¶
Returns a list of CLI parameters to apply a list of shaders
Source code in lutris/runners/mame.py
def get_shader_params(self, shader_dir, shaders):
"""Returns a list of CLI parameters to apply a list of shaders"""
params = []
shader_path = os.path.join(self.working_dir, "shaders", shader_dir)
for index, shader in enumerate(shaders):
params += [
"-gl_glsl",
"-glsl_shader_mame%s" % index,
os.path.join(shader_path, shader)
]
return params
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/mame.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
AsyncCall(write_mame_xml, notify_mame_xml)
super().install(version=version, downloader=downloader, callback=on_runner_installed)
play(self)
¶
Source code in lutris/runners/mame.py
def play(self):
command = [self.get_executable(), "-skip_gameinfo", "-inipath", self.config_dir]
if self.runner_config.get("video"):
command += ["-video", self.runner_config["video"]]
if not self.runner_config.get("fullscreen"):
command.append("-window")
if self.runner_config.get("waitvsync"):
command.append("-waitvsync")
if self.runner_config.get("uimodekey"):
command += ["-uimodekey", self.runner_config["uimodekey"]]
if self.runner_config.get("crt"):
command += self.get_shader_params("CRT-geom", ["Gaussx", "Gaussy", "CRT-geom-halation"])
command += ["-nounevenstretch"]
if self.game_config.get("machine"):
rompath = self.runner_config.get("rompath")
if rompath:
command += ["-rompath", rompath]
command.append(self.game_config["machine"])
device = self.game_config.get("device")
if not device:
return {'error': "CUSTOM", "text": "No device is set for machine %s" % self.game_config["machine"]}
rom = self.game_config.get("main_file")
if rom:
command += ["-" + device, rom]
else:
rompath = os.path.dirname(self.game_config.get("main_file"))
if not rompath:
rompath = self.runner_config.get("rompath")
rom = os.path.basename(self.game_config.get("main_file"))
if not rompath:
return {'error': 'PATH_NOT_SET', 'path': 'rompath'}
command += ["-rompath", rompath, rom]
if self.game_config.get("autoboot_command"):
command += ["-autoboot_command", self.game_config["autoboot_command"] + "\\n"]
if self.game_config.get("autoboot_delay"):
command += ["-autoboot_delay", str(self.game_config["autoboot_delay"])]
for arg in split_arguments(self.game_config.get("args")):
command.append(arg)
return {"command": command}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/mame.py
def prelaunch(self):
if not system.path_exists(os.path.join(self.config_dir, "mame.ini")):
try:
os.makedirs(self.config_dir)
except OSError:
pass
system.execute(
[self.get_executable(), "-createconfig", "-inipath", self.config_dir],
env=runtime.get_env(),
cwd=self.working_dir
)
return True
write_xml_list(self)
¶
Write the full game list in XML to disk
Source code in lutris/runners/mame.py
def write_xml_list(self):
"""Write the full game list in XML to disk"""
os.makedirs(self.cache_dir, exist_ok=True)
output = system.execute(
[self.get_executable(), "-listxml"],
env=runtime.get_env()
)
if output:
with open(self.xml_path, "w", encoding='utf-8') as xml_file:
xml_file.write(output)
logger.info("MAME XML list written to %s", self.xml_path)
else:
logger.warning("Couldn't get any output for mame -listxml")
get_system_choices(include_year=True)
¶
Return list of systems for inclusion in dropdown
Source code in lutris/runners/mame.py
def get_system_choices(include_year=True):
"""Return list of systems for inclusion in dropdown"""
if not system.path_exists(MAME_XML_PATH, exclude_empty=True):
mame_inst = mame()
if mame_inst.is_installed():
AsyncCall(write_mame_xml, notify_mame_xml)
return []
for system_id, info in sorted(
get_supported_systems(MAME_XML_PATH).items(),
key=lambda sys: (sys[1]["manufacturer"], sys[1]["description"]),
):
if info["description"].startswith(info["manufacturer"]):
template = ""
else:
template = "%(manufacturer)s "
template += "%(description)s"
if include_year:
template += " %(year)s"
system_name = template % info
system_name = system_name.replace("<generic>", "").strip()
yield (system_name, system_id)
notify_mame_xml(result, error)
¶
Source code in lutris/runners/mame.py
def notify_mame_xml(result, error):
if error:
logger.error("Failed writing MAME XML")
elif result:
logger.info("Finished writing MAME XML")
write_mame_xml(force=False)
¶
Source code in lutris/runners/mame.py
def write_mame_xml(force=False):
if not system.path_exists(MAME_CACHE_DIR):
system.create_folder(MAME_CACHE_DIR)
if system.path_exists(MAME_XML_PATH, exclude_empty=True) and not force:
return False
logger.info("Writing full game list from MAME to %s", MAME_XML_PATH)
mame_inst = mame()
mame_inst.write_xml_list()
if system.get_disk_size(MAME_XML_PATH) == 0:
logger.warning("MAME did not write anything to %s", MAME_XML_PATH)
return False
return True
mednafen
¶
DEFAULT_MEDNAFEN_SCALER
¶
mednafen (Runner)
¶
Source code in lutris/runners/mednafen.py
class mednafen(Runner):
human_name = _("Mednafen")
description = _("Multi-system emulator: NES, PC Engine, PSX…")
platforms = [
_("Nintendo Game Boy (Color)"),
_("Nintendo Game Boy Advance"),
_("Sega Game Gear"),
_("Sega Genesis/Mega Drive"),
_("Atari Lynx"),
_("Sega Master System"),
_("SNK Neo Geo Pocket (Color)"),
_("Nintendo NES"),
_("NEC PC Engine TurboGrafx-16"),
_("NEC PC-FX"),
_("Sony PlayStation"),
_("Sega Saturn"),
_("Nintendo SNES"),
_("Bandai WonderSwan"),
_("Nintendo Virtual Boy"),
]
machine_choices = (
(_("Game Boy (Color)"), "gb"),
(_("Game Boy Advance"), "gba"),
(_("Game Gear"), "gg"),
(_("Genesis/Mega Drive"), "md"),
(_("Lynx"), "lynx"),
(_("Master System"), "sms"),
(_("Neo Geo Pocket (Color)"), "gnp"),
(_("NES"), "nes"),
(_("PC Engine"), "pce_fast"),
(_("PC-FX"), "pcfx"),
(_("PlayStation"), "psx"),
(_("Saturn"), "ss"),
(_("SNES"), "snes"),
(_("WonderSwan"), "wswan"),
(_("Virtual Boy"), "vb"),
)
runner_executable = "mednafen/bin/mednafen"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"help":
_("The game data, commonly called a ROM image. \n"
"Mednafen supports GZIP and ZIP compressed ROMs."),
},
{
"option": "machine",
"type": "choice",
"label": _("Machine type"),
"choices": machine_choices,
"help": _("The emulated machine."),
},
]
runner_options = [
{
"option": "fs",
"type": "bool",
"label": _("Fullscreen"),
"default": False
},
{
"option":
"stretch",
"type":
"choice",
"label":
_("Aspect ratio"),
"choices": (
(_("Disabled"), "0"),
(_("Stretched"), "full"),
(_("Preserve aspect ratio"), "aspect"),
(_("Integer scale"), "aspect_int"),
(_("Multiple of 2 scale"), "aspect_mult2"),
),
"default":
"aspect_int",
},
{
"option":
"scaler",
"type":
"choice",
"label":
_("Video scaler"),
"choices": (
("none", "none"),
("hq2x", "hq2x"),
("hq3x", "hq3x"),
("hq4x", "hq4x"),
("scale2x", "scale2x"),
("scale3x", "scale3x"),
("scale4x", "scale4x"),
("2xsai", "2xsai"),
("super2xsai", "super2xsai"),
("supereagle", "supereagle"),
("nn2x", "nn2x"),
("nn3x", "nn3x"),
("nn4x", "nn4x"),
("nny2x", "nny2x"),
("nny3x", "nny3x"),
("nny4x", "nny4x"),
),
"default":
DEFAULT_MEDNAFEN_SCALER,
},
{
"option":
"sound_device",
"type":
"choice",
"label":
_("Sound device"),
"choices": (
(_("Mednafen default"), "default"),
(_("ALSA default"), "sexyal-literal-default"),
("hw:0", "hw:0,0"),
("hw:1", "hw:1,0"),
("hw:2", "hw:2,0"),
),
"default":
"sexyal-literal-default"
},
{
"option": "dont_map_controllers",
"type": "bool",
"label": _("Use default Mednafen controller configuration"),
"default": False,
},
]
def get_platform(self):
machine = self.game_config.get("machine")
if machine:
for index, choice in enumerate(self.machine_choices):
if choice[1] == machine:
return self.platforms[index]
return ""
def find_joysticks(self):
""" Detect connected joysticks and return their ids """
joy_ids = []
if not self.is_installed:
return []
with subprocess.Popen(
[self.get_executable(), "dummy"],
stdout=subprocess.PIPE,
universal_newlines=True,
) as mednafen_process:
output = mednafen_process.communicate()[0].split("\n")
found = False
joy_list = []
for line in output:
if found and "Joystick" in line:
joy_list.append(line)
else:
found = False
if "Initializing joysticks" in line:
found = True
for joy in joy_list:
index = joy.find("Unique ID:")
joy_id = joy[index + 11:]
logger.debug("Joystick found id %s ", joy_id)
joy_ids.append(joy_id)
return joy_ids
@staticmethod
def set_joystick_controls(joy_ids, machine):
""" Setup joystick mappings per machine """
# Get the controller mappings
controller_mappings = get_controller_mappings()
if not controller_mappings:
logger.warning("No controller detected for joysticks %s.", joy_ids)
return []
# TODO currently only supports the first controller. Add support for other controllers.
mapping = controller_mappings[0][1]
# Construct a dictionnary of button codes to parse to mendafen
map_code = {
"a": "",
"b": "",
"c": "",
"x": "",
"y": "",
"z": "",
"back": "",
"start": "",
"leftshoulder": "",
"rightshoulder": "",
"lefttrigger": "",
"righttrigger": "",
"leftstick": "",
"rightstick": "",
"select": "",
"shoulder_l": "",
"shoulder_r": "",
"i": "",
"ii": "",
"iii": "",
"iv": "",
"v": "",
"vi": "",
"run": "",
"ls": "",
"rs": "",
"fire1": "",
"fire2": "",
"option_1": "",
"option_2": "",
"cross": "",
"circle": "",
"square": "",
"triangle": "",
"r1": "",
"r2": "",
"l1": "",
"l2": "",
"option": "",
"l": "",
"r": "",
"right-x": "",
"right-y": "",
"left-x": "",
"left-y": "",
"up-x": "",
"up-y": "",
"down-x": "",
"down-y": "",
"up-l": "",
"up-r": "",
"down-l": "",
"down-r": "",
"left-l": "",
"left-r": "",
"right-l": "",
"right-r": "",
"lstick_up": "0000c001",
"lstick_down": "00008001",
"lstick_right": "00008000",
"lstick_left": "0000c000",
"rstick_up": "0000c003",
"rstick_down": "00008003",
"rstick_left": "0000c002",
"rstick_right": "00008002",
"dpup": "0000c005",
"dpdown": "00008005",
"dpleft": "0000c004",
"dpright": "00008004",
}
# Insert the button mapping number into the map_codes
for button in mapping.keys:
bttn_id = mapping.keys[button]
if bttn_id[0] == "b": # it's a button
map_code[button] = "000000" + bttn_id[1:].zfill(2)
# Duplicate button names that are emulated in mednanfen
map_code["up"] = map_code["dpup"] #
map_code["down"] = map_code["dpdown"] #
map_code["left"] = map_code["dpleft"] # Multiple systems
map_code["right"] = map_code["dpright"]
map_code["select"] = map_code["back"] #
map_code["shoulder_r"] = map_code["rightshoulder"] # GBA
map_code["shoulder_l"] = map_code["leftshoulder"] #
map_code["i"] = map_code["b"] #
map_code["ii"] = map_code["a"] #
map_code["iii"] = map_code["leftshoulder"]
map_code["iv"] = map_code["y"] # PCEngine and PCFX
map_code["v"] = map_code["x"] #
map_code["vi"] = map_code["rightshoulder"]
map_code["run"] = map_code["start"] #
map_code["ls"] = map_code["leftshoulder"] #
map_code["rs"] = map_code["rightshoulder"] # Saturn
map_code["c"] = map_code["righttrigger"] #
map_code["z"] = map_code["lefttrigger"] #
map_code["fire1"] = map_code["a"] # Master System
map_code["fire2"] = map_code["b"] #
map_code["option_1"] = map_code["x"] # Lynx
map_code["option_2"] = map_code["y"] #
map_code["r1"] = map_code["rightshoulder"] #
map_code["r2"] = map_code["righttrigger"] #
map_code["l1"] = map_code["leftshoulder"] #
map_code["l2"] = map_code["lefttrigger"] # PlayStation
map_code["cross"] = map_code["a"] #
map_code["circle"] = map_code["b"] #
map_code["square"] = map_code["x"] #
map_code["triangle"] = map_code["y"] #
map_code["option"] = map_code["select"] # NeoGeo pocket
map_code["l"] = map_code["leftshoulder"] # SNES
map_code["r"] = map_code["rightshoulder"] #
map_code["right-x"] = map_code["dpright"] #
map_code["left-x"] = map_code["dpleft"] #
map_code["up-x"] = map_code["dpup"] #
map_code["down-x"] = map_code["dpdown"] # Wonder Swan
map_code["right-y"] = map_code["lstick_right"]
map_code["left-y"] = map_code["lstick_left"] #
map_code["up-y"] = map_code["lstick_up"] #
map_code["down-y"] = map_code["lstick_down"] #
map_code["up-l"] = map_code["dpup"] #
map_code["down-l"] = map_code["dpdown"] #
map_code["left-l"] = map_code["dpleft"] #
map_code["right-l"] = map_code["dpright"] #
map_code["up-r"] = map_code["rstick_up"] #
map_code["down-r"] = map_code["rstick_down"] # Virtual boy
map_code["left-r"] = map_code["rstick_left"] #
map_code["right-r"] = map_code["rstick_right"] #
map_code["lt"] = map_code["leftshoulder"] #
map_code["rt"] = map_code["rightshoulder"] #
# Define which buttons to use for each machine
layout = {
"nes": ["a", "b", "start", "select", "up", "down", "left", "right"],
"gb": ["a", "b", "start", "select", "up", "down", "left", "right"],
"gba": [
"a",
"b",
"shoulder_r",
"shoulder_l",
"start",
"select",
"up",
"down",
"left",
"right",
],
"pce": [
"i",
"ii",
"iii",
"iv",
"v",
"vi",
"run",
"select",
"up",
"down",
"left",
"right",
],
"ss": [
"a",
"b",
"c",
"x",
"y",
"z",
"ls",
"rs",
"start",
"up",
"down",
"left",
"right",
],
"gg": ["button1", "button2", "start", "up", "down", "left", "right"],
"md": [
"a",
"b",
"c",
"x",
"y",
"z",
"start",
"up",
"down",
"left",
"right",
],
"sms": ["fire1", "fire2", "up", "down", "left", "right"],
"lynx": ["a", "b", "option_1", "option_2", "up", "down", "left", "right"],
"psx": [
"cross",
"circle",
"square",
"triangle",
"l1",
"l2",
"r1",
"r2",
"start",
"select",
"lstick_up",
"lstick_down",
"lstick_right",
"lstick_left",
"rstick_up",
"rstick_down",
"rstick_left",
"rstick_right",
"up",
"down",
"left",
"right",
],
"pcfx": [
"i",
"ii",
"iii",
"iv",
"v",
"vi",
"run",
"select",
"up",
"down",
"left",
"right",
],
"ngp": ["a", "b", "option", "up", "down", "left", "right"],
"snes": [
"a",
"b",
"x",
"y",
"l",
"r",
"start",
"select",
"up",
"down",
"left",
"right",
],
"wswan": [
"a",
"b",
"right-x",
"right-y",
"left-x",
"left-y",
"up-x",
"up-y",
"down-x",
"down-y",
"start",
],
"vb": [
"up-l",
"down-l",
"left-l",
"right-l",
"up-r",
"down-r",
"left-r",
"right-r",
"a",
"b",
"lt",
"rt",
],
}
# Select a the gamepad type
controls = []
if machine in ["gg", "lynx", "wswan", "gb", "gba", "vb"]:
gamepad = "builtin.gamepad"
elif machine in ["md"]:
gamepad = "port1.gamepad6"
controls.append("-md.input.port1")
controls.append("gamepad6")
elif machine in ["psx"]:
gamepad = "port1.dualshock"
controls.append("-psx.input.port1")
controls.append("dualshock")
else:
gamepad = "port1.gamepad"
# Construct the controlls options
for button in layout[machine]:
controls.append("-{}.input.{}.{}".format(machine, gamepad, button))
controls.append("joystick {} {}".format(joy_ids[0], map_code[button]))
return controls
def play(self):
"""Runs the game"""
rom = self.game_config.get("main_file") or ""
machine = self.game_config.get("machine") or ""
fullscreen = self.runner_config.get("fs") or "0"
if fullscreen is True:
fullscreen = "1"
elif fullscreen is False:
fullscreen = "0"
stretch = self.runner_config.get("stretch") or "0"
scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
sound_device = self.runner_config.get("sound_device")
xres, yres = DISPLAY_MANAGER.get_current_resolution()
options = [
"-fs",
fullscreen,
"-force_module",
machine,
"-sound.device",
sound_device,
"-" + machine + ".xres",
xres,
"-" + machine + ".yres",
yres,
"-" + machine + ".stretch",
stretch,
"-" + machine + ".special",
scaler,
"-" + machine + ".videoip",
"1",
]
joy_ids = self.find_joysticks()
dont_map_controllers = self.runner_config.get("dont_map_controllers")
if joy_ids and not dont_map_controllers:
controls = self.set_joystick_controls(joy_ids, machine)
for control in controls:
options.append(control)
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
command = [self.get_executable()]
for option in options:
command.append(option)
command.append(rom)
return {"command": command}
description
¶
game_options
¶
human_name
¶
machine_choices
¶
platforms
¶
runner_executable
¶
runner_options
¶
find_joysticks(self)
¶
Detect connected joysticks and return their ids
Source code in lutris/runners/mednafen.py
def find_joysticks(self):
""" Detect connected joysticks and return their ids """
joy_ids = []
if not self.is_installed:
return []
with subprocess.Popen(
[self.get_executable(), "dummy"],
stdout=subprocess.PIPE,
universal_newlines=True,
) as mednafen_process:
output = mednafen_process.communicate()[0].split("\n")
found = False
joy_list = []
for line in output:
if found and "Joystick" in line:
joy_list.append(line)
else:
found = False
if "Initializing joysticks" in line:
found = True
for joy in joy_list:
index = joy.find("Unique ID:")
joy_id = joy[index + 11:]
logger.debug("Joystick found id %s ", joy_id)
joy_ids.append(joy_id)
return joy_ids
get_platform(self)
¶
Source code in lutris/runners/mednafen.py
def get_platform(self):
machine = self.game_config.get("machine")
if machine:
for index, choice in enumerate(self.machine_choices):
if choice[1] == machine:
return self.platforms[index]
return ""
play(self)
¶
Runs the game
Source code in lutris/runners/mednafen.py
def play(self):
"""Runs the game"""
rom = self.game_config.get("main_file") or ""
machine = self.game_config.get("machine") or ""
fullscreen = self.runner_config.get("fs") or "0"
if fullscreen is True:
fullscreen = "1"
elif fullscreen is False:
fullscreen = "0"
stretch = self.runner_config.get("stretch") or "0"
scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
sound_device = self.runner_config.get("sound_device")
xres, yres = DISPLAY_MANAGER.get_current_resolution()
options = [
"-fs",
fullscreen,
"-force_module",
machine,
"-sound.device",
sound_device,
"-" + machine + ".xres",
xres,
"-" + machine + ".yres",
yres,
"-" + machine + ".stretch",
stretch,
"-" + machine + ".special",
scaler,
"-" + machine + ".videoip",
"1",
]
joy_ids = self.find_joysticks()
dont_map_controllers = self.runner_config.get("dont_map_controllers")
if joy_ids and not dont_map_controllers:
controls = self.set_joystick_controls(joy_ids, machine)
for control in controls:
options.append(control)
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
command = [self.get_executable()]
for option in options:
command.append(option)
command.append(rom)
return {"command": command}
set_joystick_controls(joy_ids, machine)
staticmethod
¶
Setup joystick mappings per machine
Source code in lutris/runners/mednafen.py
@staticmethod
def set_joystick_controls(joy_ids, machine):
""" Setup joystick mappings per machine """
# Get the controller mappings
controller_mappings = get_controller_mappings()
if not controller_mappings:
logger.warning("No controller detected for joysticks %s.", joy_ids)
return []
# TODO currently only supports the first controller. Add support for other controllers.
mapping = controller_mappings[0][1]
# Construct a dictionnary of button codes to parse to mendafen
map_code = {
"a": "",
"b": "",
"c": "",
"x": "",
"y": "",
"z": "",
"back": "",
"start": "",
"leftshoulder": "",
"rightshoulder": "",
"lefttrigger": "",
"righttrigger": "",
"leftstick": "",
"rightstick": "",
"select": "",
"shoulder_l": "",
"shoulder_r": "",
"i": "",
"ii": "",
"iii": "",
"iv": "",
"v": "",
"vi": "",
"run": "",
"ls": "",
"rs": "",
"fire1": "",
"fire2": "",
"option_1": "",
"option_2": "",
"cross": "",
"circle": "",
"square": "",
"triangle": "",
"r1": "",
"r2": "",
"l1": "",
"l2": "",
"option": "",
"l": "",
"r": "",
"right-x": "",
"right-y": "",
"left-x": "",
"left-y": "",
"up-x": "",
"up-y": "",
"down-x": "",
"down-y": "",
"up-l": "",
"up-r": "",
"down-l": "",
"down-r": "",
"left-l": "",
"left-r": "",
"right-l": "",
"right-r": "",
"lstick_up": "0000c001",
"lstick_down": "00008001",
"lstick_right": "00008000",
"lstick_left": "0000c000",
"rstick_up": "0000c003",
"rstick_down": "00008003",
"rstick_left": "0000c002",
"rstick_right": "00008002",
"dpup": "0000c005",
"dpdown": "00008005",
"dpleft": "0000c004",
"dpright": "00008004",
}
# Insert the button mapping number into the map_codes
for button in mapping.keys:
bttn_id = mapping.keys[button]
if bttn_id[0] == "b": # it's a button
map_code[button] = "000000" + bttn_id[1:].zfill(2)
# Duplicate button names that are emulated in mednanfen
map_code["up"] = map_code["dpup"] #
map_code["down"] = map_code["dpdown"] #
map_code["left"] = map_code["dpleft"] # Multiple systems
map_code["right"] = map_code["dpright"]
map_code["select"] = map_code["back"] #
map_code["shoulder_r"] = map_code["rightshoulder"] # GBA
map_code["shoulder_l"] = map_code["leftshoulder"] #
map_code["i"] = map_code["b"] #
map_code["ii"] = map_code["a"] #
map_code["iii"] = map_code["leftshoulder"]
map_code["iv"] = map_code["y"] # PCEngine and PCFX
map_code["v"] = map_code["x"] #
map_code["vi"] = map_code["rightshoulder"]
map_code["run"] = map_code["start"] #
map_code["ls"] = map_code["leftshoulder"] #
map_code["rs"] = map_code["rightshoulder"] # Saturn
map_code["c"] = map_code["righttrigger"] #
map_code["z"] = map_code["lefttrigger"] #
map_code["fire1"] = map_code["a"] # Master System
map_code["fire2"] = map_code["b"] #
map_code["option_1"] = map_code["x"] # Lynx
map_code["option_2"] = map_code["y"] #
map_code["r1"] = map_code["rightshoulder"] #
map_code["r2"] = map_code["righttrigger"] #
map_code["l1"] = map_code["leftshoulder"] #
map_code["l2"] = map_code["lefttrigger"] # PlayStation
map_code["cross"] = map_code["a"] #
map_code["circle"] = map_code["b"] #
map_code["square"] = map_code["x"] #
map_code["triangle"] = map_code["y"] #
map_code["option"] = map_code["select"] # NeoGeo pocket
map_code["l"] = map_code["leftshoulder"] # SNES
map_code["r"] = map_code["rightshoulder"] #
map_code["right-x"] = map_code["dpright"] #
map_code["left-x"] = map_code["dpleft"] #
map_code["up-x"] = map_code["dpup"] #
map_code["down-x"] = map_code["dpdown"] # Wonder Swan
map_code["right-y"] = map_code["lstick_right"]
map_code["left-y"] = map_code["lstick_left"] #
map_code["up-y"] = map_code["lstick_up"] #
map_code["down-y"] = map_code["lstick_down"] #
map_code["up-l"] = map_code["dpup"] #
map_code["down-l"] = map_code["dpdown"] #
map_code["left-l"] = map_code["dpleft"] #
map_code["right-l"] = map_code["dpright"] #
map_code["up-r"] = map_code["rstick_up"] #
map_code["down-r"] = map_code["rstick_down"] # Virtual boy
map_code["left-r"] = map_code["rstick_left"] #
map_code["right-r"] = map_code["rstick_right"] #
map_code["lt"] = map_code["leftshoulder"] #
map_code["rt"] = map_code["rightshoulder"] #
# Define which buttons to use for each machine
layout = {
"nes": ["a", "b", "start", "select", "up", "down", "left", "right"],
"gb": ["a", "b", "start", "select", "up", "down", "left", "right"],
"gba": [
"a",
"b",
"shoulder_r",
"shoulder_l",
"start",
"select",
"up",
"down",
"left",
"right",
],
"pce": [
"i",
"ii",
"iii",
"iv",
"v",
"vi",
"run",
"select",
"up",
"down",
"left",
"right",
],
"ss": [
"a",
"b",
"c",
"x",
"y",
"z",
"ls",
"rs",
"start",
"up",
"down",
"left",
"right",
],
"gg": ["button1", "button2", "start", "up", "down", "left", "right"],
"md": [
"a",
"b",
"c",
"x",
"y",
"z",
"start",
"up",
"down",
"left",
"right",
],
"sms": ["fire1", "fire2", "up", "down", "left", "right"],
"lynx": ["a", "b", "option_1", "option_2", "up", "down", "left", "right"],
"psx": [
"cross",
"circle",
"square",
"triangle",
"l1",
"l2",
"r1",
"r2",
"start",
"select",
"lstick_up",
"lstick_down",
"lstick_right",
"lstick_left",
"rstick_up",
"rstick_down",
"rstick_left",
"rstick_right",
"up",
"down",
"left",
"right",
],
"pcfx": [
"i",
"ii",
"iii",
"iv",
"v",
"vi",
"run",
"select",
"up",
"down",
"left",
"right",
],
"ngp": ["a", "b", "option", "up", "down", "left", "right"],
"snes": [
"a",
"b",
"x",
"y",
"l",
"r",
"start",
"select",
"up",
"down",
"left",
"right",
],
"wswan": [
"a",
"b",
"right-x",
"right-y",
"left-x",
"left-y",
"up-x",
"up-y",
"down-x",
"down-y",
"start",
],
"vb": [
"up-l",
"down-l",
"left-l",
"right-l",
"up-r",
"down-r",
"left-r",
"right-r",
"a",
"b",
"lt",
"rt",
],
}
# Select a the gamepad type
controls = []
if machine in ["gg", "lynx", "wswan", "gb", "gba", "vb"]:
gamepad = "builtin.gamepad"
elif machine in ["md"]:
gamepad = "port1.gamepad6"
controls.append("-md.input.port1")
controls.append("gamepad6")
elif machine in ["psx"]:
gamepad = "port1.dualshock"
controls.append("-psx.input.port1")
controls.append("dualshock")
else:
gamepad = "port1.gamepad"
# Construct the controlls options
for button in layout[machine]:
controls.append("-{}.input.{}.{}".format(machine, gamepad, button))
controls.append("joystick {} {}".format(joy_ids[0], map_code[button]))
return controls
mupen64plus
¶
mupen64plus (Runner)
¶
Source code in lutris/runners/mupen64plus.py
class mupen64plus(Runner):
human_name = _("Mupen64Plus")
description = _("Nintendo 64 emulator")
platforms = [_("Nintendo 64")]
runner_executable = "mupen64plus/mupen64plus"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"help": _("The game data, commonly called a ROM image."),
}
]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": True,
},
{
"option": "hideosd",
"type": "bool",
"label": _("Hide OSD"),
"default": True
},
]
@property
def working_dir(self):
return os.path.join(settings.RUNNER_DIR, "mupen64plus")
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("hideosd"):
arguments.append("--noosd")
else:
arguments.append("--osd")
if self.runner_config.get("fullscreen"):
arguments.append("--fullscreen")
else:
arguments.append("--windowed")
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
play(self)
¶
Source code in lutris/runners/mupen64plus.py
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("hideosd"):
arguments.append("--noosd")
else:
arguments.append("--osd")
if self.runner_config.get("fullscreen"):
arguments.append("--fullscreen")
else:
arguments.append("--windowed")
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
o2em
¶
o2em (Runner)
¶
Source code in lutris/runners/o2em.py
class o2em(Runner):
human_name = _("O2EM")
description = _("Magnavox Odyssey² Emulator")
platforms = (
_("Magnavox Odyssey²"),
_("Phillips C52"),
_("Phillips Videopac+"),
_("Brandt Jopac"),
)
bios_path = os.path.expanduser("~/.o2em/bios")
runner_executable = "o2em/o2em"
checksums = {
"o2rom": "562d5ebf9e030a40d6fabfc2f33139fd",
"c52": "f1071cdb0b6b10dde94d3bc8a6146387",
"jopac": "279008e4a0db2dc5f1c048853b033828",
"g7400": "79008e4a0db2dc5f1c048853b033828",
}
bios_choices = [
(_("Magnavox Odyssey²"), "o2rom"),
(_("Phillips C52"), "c52"),
(_("Phillips Videopac+"), "g7400"),
(_("Brandt Jopac"), "jopac"),
]
controller_choices = [
(_("Disable"), "0"),
(_("Arrow Keys and Right Shift"), "1"),
(_("W,S,A,D,SPACE"), "2"),
(_("Joystick"), "3"),
]
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"default_path": "game_path",
"help": _("The game data, commonly called a ROM image."),
}
]
runner_options = [
{
"option": "bios",
"type": "choice",
"choices": bios_choices,
"label": _("BIOS"),
"default": "o2rom",
},
{
"option": "controller1",
"type": "choice",
"choices": controller_choices,
"label": _("First controller"),
"default": "2",
},
{
"option": "controller2",
"type": "choice",
"choices": controller_choices,
"label": _("Second controller"),
"default": "1",
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "scanlines",
"type": "bool",
"label": _("Scanlines display style"),
"default": False,
"help": _("Activates a display filter adding scanlines to imitate "
"the displays of yesteryear."),
},
]
def get_platform(self):
bios = self.runner_config.get("bios")
if bios:
for i, b in enumerate(self.bios_choices):
if b[1] == bios:
return self.platforms[i]
return ""
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
if not system.path_exists(self.bios_path):
os.makedirs(self.bios_path)
if callback:
callback()
super().install(version, downloader, on_runner_installed)
def play(self):
arguments = ["-biosdir=%s" % self.bios_path]
if self.runner_config.get("fullscreen"):
arguments.append("-fullscreen")
if self.runner_config.get("scanlines"):
arguments.append("-scanlines")
if "controller1" in self.runner_config:
arguments.append("-s1=%s" % self.runner_config["controller1"])
if "controller2" in self.runner_config:
arguments.append("-s2=%s" % self.runner_config["controller2"])
rom_path = self.game_config.get("main_file") or ""
if not system.path_exists(rom_path):
return {"error": "FILE_NOT_FOUND", "file": rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)
arguments.append("-romdir=%s/" % romdir)
arguments.append(romfile)
return {"command": [self.get_executable()] + arguments}
bios_choices
¶
bios_path
¶
checksums
¶
controller_choices
¶
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
get_platform(self)
¶
Source code in lutris/runners/o2em.py
def get_platform(self):
bios = self.runner_config.get("bios")
if bios:
for i, b in enumerate(self.bios_choices):
if b[1] == bios:
return self.platforms[i]
return ""
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/o2em.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
if not system.path_exists(self.bios_path):
os.makedirs(self.bios_path)
if callback:
callback()
super().install(version, downloader, on_runner_installed)
play(self)
¶
Source code in lutris/runners/o2em.py
def play(self):
arguments = ["-biosdir=%s" % self.bios_path]
if self.runner_config.get("fullscreen"):
arguments.append("-fullscreen")
if self.runner_config.get("scanlines"):
arguments.append("-scanlines")
if "controller1" in self.runner_config:
arguments.append("-s1=%s" % self.runner_config["controller1"])
if "controller2" in self.runner_config:
arguments.append("-s2=%s" % self.runner_config["controller2"])
rom_path = self.game_config.get("main_file") or ""
if not system.path_exists(rom_path):
return {"error": "FILE_NOT_FOUND", "file": rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)
arguments.append("-romdir=%s/" % romdir)
arguments.append(romfile)
return {"command": [self.get_executable()] + arguments}
openmsx
¶
openmsx (Runner)
¶
Source code in lutris/runners/openmsx.py
class openmsx(Runner):
human_name = _("openMSX")
description = _("MSX computer emulator")
platforms = [_("MSX, MSX2, MSX2+, MSX turboR")]
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"help": _("The game data, commonly called a ROM image."),
}
]
def play(self):
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
return {"command": [self.get_executable(), rom]}
osmose
¶
osmose (Runner)
¶
Source code in lutris/runners/osmose.py
class osmose(Runner):
human_name = _("Osmose")
description = _("Sega Master System Emulator")
platforms = [_("Sega Master System")]
runner_executable = "osmose/osmose"
game_options = [
{
"option":
"main_file",
"type":
"file",
"label":
_("ROM file"),
"default_path":
"game_path",
"help": _(
"The game data, commonly called a ROM image.\n"
"Supported formats: SMS and GG files. ZIP compressed "
"ROMs are supported."
),
}
]
runner_options = [{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
}]
def play(self):
"""Run Sega Master System game"""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
if self.runner_config.get("fullscreen"):
arguments.append("-fs")
arguments.append("-bilinear")
return {"command": arguments}
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
play(self)
¶
Run Sega Master System game
Source code in lutris/runners/osmose.py
def play(self):
"""Run Sega Master System game"""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
if self.runner_config.get("fullscreen"):
arguments.append("-fs")
arguments.append("-bilinear")
return {"command": arguments}
pcsx2
¶
pcsx2 (Runner)
¶
Source code in lutris/runners/pcsx2.py
class pcsx2(Runner):
human_name = _("PCSX2")
description = _("PlayStation 2 emulator")
platforms = [_("Sony PlayStation 2")]
runnable_alone = True
runner_executable = "pcsx2/PCSX2"
arch = "i386"
require_libs = ["libOpenGL.so.0", "libgdk-x11-2.0.so.0", "libEGL.so.1"]
game_options = [{
"option": "main_file",
"type": "file",
"label": _("ISO file"),
"default_path": "game_path",
}]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "full_boot",
"type": "bool",
"label": _("Fullboot"),
"default": False
},
{
"option": "nogui",
"type": "bool",
"label": _("No GUI"),
"default": False
},
{
"option": "config_file",
"type": "file",
"label": _("Custom config file"),
"advanced": True,
},
{
"option": "config_path",
"type": "directory_chooser",
"label": _("Custom config path"),
"advanced": True,
},
]
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("fullscreen"):
arguments.append("--fullscreen")
if self.runner_config.get("full_boot"):
arguments.append("--fullboot")
if self.runner_config.get("nogui"):
arguments.append("--nogui")
if self.runner_config.get("config_file"):
arguments.append("--cfg={}".format(self.runner_config["config_file"]))
if self.runner_config.get("config_path"):
arguments.append("--cfgpath={}".format(self.runner_config["config_path"]))
iso = self.game_config.get("main_file") or ""
if not system.path_exists(iso):
return {"error": "FILE_NOT_FOUND", "file": iso}
arguments.append(iso)
return {"command": arguments}
arch
¶
description
¶
game_options
¶
human_name
¶
platforms
¶
require_libs
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
play(self)
¶
Source code in lutris/runners/pcsx2.py
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("fullscreen"):
arguments.append("--fullscreen")
if self.runner_config.get("full_boot"):
arguments.append("--fullboot")
if self.runner_config.get("nogui"):
arguments.append("--nogui")
if self.runner_config.get("config_file"):
arguments.append("--cfg={}".format(self.runner_config["config_file"]))
if self.runner_config.get("config_path"):
arguments.append("--cfgpath={}".format(self.runner_config["config_path"]))
iso = self.game_config.get("main_file") or ""
if not system.path_exists(iso):
return {"error": "FILE_NOT_FOUND", "file": iso}
arguments.append(iso)
return {"command": arguments}
pico8
¶
Runner for the PICO-8 fantasy console
DOWNLOAD_URL
¶
pico8 (Runner)
¶
Source code in lutris/runners/pico8.py
class pico8(Runner):
description = _("Runs PICO-8 fantasy console cartridges")
multiple_versions = False
human_name = _("PICO-8")
platforms = [_("PICO-8")]
game_options = [
{
"option": "main_file",
"type": "string",
"label": _("Cartridge file/URL/ID"),
"help": _("You can put a .p8.png file path, URL, or BBS cartridge ID here."),
}
]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": True,
"help": _("Launch in fullscreen."),
},
{
"option": "window_size",
"label": _("Window size"),
"type": "string",
"default": "640x512",
"help": _("The initial size of the game window."),
},
{
"option": "splore",
"type": "bool",
"label": _("Start in splore mode"),
"default": False,
},
{
"option": "args",
"type": "string",
"label": _("Extra arguments"),
"default": "",
"help": _("Extra arguments to the executable"),
"advanced": True,
},
{
"option": "engine",
"type": "string",
"label": _("Engine (web only)"),
"default": "pico8_0111g_4",
"help": _("Name of engine (will be downloaded) or local file path"),
},
]
system_options_override = [{"option": "disable_runtime", "default": True}]
runner_executable = "pico8/web.py"
def __init__(self, config=None):
super().__init__(config)
self.runnable_alone = self.is_native
def __repr__(self):
return _("PICO-8 runner (%s)") % self.config
def install(self, version=None, downloader=None, callback=None):
opts = {}
if callback:
opts["callback"] = callback
opts["dest"] = settings.RUNNER_DIR + "/pico8"
opts["merge_single"] = True
if downloader:
opts["downloader"] = downloader
else:
raise RuntimeError("Unsupported download for this runner")
self.download_and_extract(DOWNLOAD_URL, **opts)
@property
def is_native(self):
return self.runner_config.get("runner_executable", "") != ""
@property
def engine_path(self):
engine = self.runner_config.get("engine")
if not engine.lower().endswith(".js") and not os.path.exists(engine):
engine = os.path.join(
settings.RUNNER_DIR,
"pico8/web/engines",
self.runner_config.get("engine") + ".js",
)
return engine
@property
def cart_path(self):
main_file = self.game_config.get("main_file")
if self.is_native and main_file.startswith("http"):
return os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")
if not os.path.exists(main_file) and main_file.isdigit():
return os.path.join(settings.RUNNER_DIR, "pico8/cartridges", main_file + ".p8.png")
return main_file
@property
def launch_args(self):
if self.is_native:
args = [self.get_executable()]
args.append("-windowed")
args.append("0" if self.runner_config.get("fullscreen") else "1")
if self.runner_config.get("splore"):
args.append("-splore")
size = self.runner_config.get("window_size").split("x")
if len(size) == 2:
args.append("-width")
args.append(size[0])
args.append("-height")
args.append(size[1])
extra_args = self.runner_config.get("args", "")
for arg in split_arguments(extra_args):
args.append(arg)
else:
args = [
self.get_executable(),
os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"),
"--window-size",
self.runner_config.get("window_size"),
]
return args
def get_run_data(self):
return {"command": self.launch_args, "env": self.get_env(os_env=False)}
def is_installed(self, version=None, fallback=True, min_version=None):
"""Checks if pico8 runner is installed and if the pico8 executable available.
"""
if self.is_native and system.path_exists(self.runner_config.get("runner_executable")):
return True
return system.path_exists(os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"))
def prelaunch(self):
if not self.game_config.get("main_file") and self.is_installed():
return True
if os.path.exists(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")):
os.remove(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png"))
# Don't download cartridge if using web backend and cart is url
if self.is_native or not self.game_config.get("main_file").startswith("http"):
if not os.path.exists(self.game_config.get("main_file")) and (
self.game_config.get("main_file").isdigit() or self.game_config.get("main_file").startswith("http")
):
if not self.game_config.get("main_file").startswith("http"):
pid = int(self.game_config.get("main_file"))
num = math.floor(pid / 10000)
downloadUrl = ("https://www.lexaloffle.com/bbs/cposts/" + str(num) + "/" + str(pid) + ".p8.png")
else:
downloadUrl = self.game_config.get("main_file")
cartPath = self.cart_path
system.create_folder(os.path.dirname(cartPath))
downloadCompleted = False
def on_downloaded_cart():
nonlocal downloadCompleted
# If we are offline we don't want an empty file to overwrite the cartridge
if dl.downloaded_size:
shutil.move(cartPath + ".download", cartPath)
else:
os.remove(cartPath + ".download")
downloadCompleted = True
dl = Downloader(
downloadUrl,
cartPath + ".download",
True,
callback=on_downloaded_cart,
)
dl.start()
# Wait for download to complete or continue if it exists (to work in offline mode)
while not os.path.exists(cartPath):
if downloadCompleted or dl.state == Downloader.ERROR:
logger.error("Could not download cartridge from %s", downloadUrl)
return False
sleep(0.1)
# Download js engine
if not self.is_native and not os.path.exists(self.runner_config.get("engine")):
enginePath = os.path.join(
settings.RUNNER_DIR,
"pico8/web/engines",
self.runner_config.get("engine") + ".js",
)
if not os.path.exists(enginePath):
downloadUrl = ("https://www.lexaloffle.com/bbs/" + self.runner_config.get("engine") + ".js")
system.create_folder(os.path.dirname(enginePath))
downloadCompleted = False
def on_downloaded_engine():
nonlocal downloadCompleted
downloadCompleted = True
dl = Downloader(downloadUrl, enginePath, True, callback=on_downloaded_engine)
dl.start()
dl.thread.join() # Doesn't actually wait until finished
# Waits for download to complete
while not os.path.exists(enginePath):
if downloadCompleted or dl.state == Downloader.ERROR:
logger.error("Could not download engine from %s", downloadUrl)
return False
sleep(0.1)
return True
def play(self):
launch_info = {}
launch_info["env"] = self.get_env(os_env=False)
game_data = get_game_by_field(self.config.game_config_id, "configpath")
command = self.launch_args
if self.is_native:
if not self.runner_config.get("splore"):
command.append("-run")
cartPath = self.cart_path
if not os.path.exists(cartPath):
return {"error": "FILE_NOT_FOUND", "file": cartPath}
command.append(cartPath)
else:
command.append("--name")
command.append(game_data.get("name") + " - PICO-8")
# icon = datapath.get_icon_path(game_data.get("slug"))
# if icon:
# command.append("--icon")
# command.append(icon)
webargs = {
"cartridge": self.cart_path,
"engine": self.engine_path,
"fullscreen": self.runner_config.get("fullscreen") is True,
}
command.append("--execjs")
command.append("load_config(" + json.dumps(webargs) + ")")
launch_info["command"] = command
return launch_info
cart_path
property
readonly
¶
description
¶
engine_path
property
readonly
¶
game_options
¶
human_name
¶
is_native
property
readonly
¶
launch_args
property
readonly
¶
multiple_versions
¶
platforms
¶
runner_executable
¶
runner_options
¶
system_options_override
¶
__init__(self, config=None)
special
¶
Source code in lutris/runners/pico8.py
def __init__(self, config=None):
super().__init__(config)
self.runnable_alone = self.is_native
__repr__(self)
special
¶
Source code in lutris/runners/pico8.py
def __repr__(self):
return _("PICO-8 runner (%s)") % self.config
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/pico8.py
def get_run_data(self):
return {"command": self.launch_args, "env": self.get_env(os_env=False)}
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/pico8.py
def install(self, version=None, downloader=None, callback=None):
opts = {}
if callback:
opts["callback"] = callback
opts["dest"] = settings.RUNNER_DIR + "/pico8"
opts["merge_single"] = True
if downloader:
opts["downloader"] = downloader
else:
raise RuntimeError("Unsupported download for this runner")
self.download_and_extract(DOWNLOAD_URL, **opts)
is_installed(self, version=None, fallback=True, min_version=None)
¶
Checks if pico8 runner is installed and if the pico8 executable available.
Source code in lutris/runners/pico8.py
def is_installed(self, version=None, fallback=True, min_version=None):
"""Checks if pico8 runner is installed and if the pico8 executable available.
"""
if self.is_native and system.path_exists(self.runner_config.get("runner_executable")):
return True
return system.path_exists(os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"))
play(self)
¶
Source code in lutris/runners/pico8.py
def play(self):
launch_info = {}
launch_info["env"] = self.get_env(os_env=False)
game_data = get_game_by_field(self.config.game_config_id, "configpath")
command = self.launch_args
if self.is_native:
if not self.runner_config.get("splore"):
command.append("-run")
cartPath = self.cart_path
if not os.path.exists(cartPath):
return {"error": "FILE_NOT_FOUND", "file": cartPath}
command.append(cartPath)
else:
command.append("--name")
command.append(game_data.get("name") + " - PICO-8")
# icon = datapath.get_icon_path(game_data.get("slug"))
# if icon:
# command.append("--icon")
# command.append(icon)
webargs = {
"cartridge": self.cart_path,
"engine": self.engine_path,
"fullscreen": self.runner_config.get("fullscreen") is True,
}
command.append("--execjs")
command.append("load_config(" + json.dumps(webargs) + ")")
launch_info["command"] = command
return launch_info
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/pico8.py
def prelaunch(self):
if not self.game_config.get("main_file") and self.is_installed():
return True
if os.path.exists(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")):
os.remove(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png"))
# Don't download cartridge if using web backend and cart is url
if self.is_native or not self.game_config.get("main_file").startswith("http"):
if not os.path.exists(self.game_config.get("main_file")) and (
self.game_config.get("main_file").isdigit() or self.game_config.get("main_file").startswith("http")
):
if not self.game_config.get("main_file").startswith("http"):
pid = int(self.game_config.get("main_file"))
num = math.floor(pid / 10000)
downloadUrl = ("https://www.lexaloffle.com/bbs/cposts/" + str(num) + "/" + str(pid) + ".p8.png")
else:
downloadUrl = self.game_config.get("main_file")
cartPath = self.cart_path
system.create_folder(os.path.dirname(cartPath))
downloadCompleted = False
def on_downloaded_cart():
nonlocal downloadCompleted
# If we are offline we don't want an empty file to overwrite the cartridge
if dl.downloaded_size:
shutil.move(cartPath + ".download", cartPath)
else:
os.remove(cartPath + ".download")
downloadCompleted = True
dl = Downloader(
downloadUrl,
cartPath + ".download",
True,
callback=on_downloaded_cart,
)
dl.start()
# Wait for download to complete or continue if it exists (to work in offline mode)
while not os.path.exists(cartPath):
if downloadCompleted or dl.state == Downloader.ERROR:
logger.error("Could not download cartridge from %s", downloadUrl)
return False
sleep(0.1)
# Download js engine
if not self.is_native and not os.path.exists(self.runner_config.get("engine")):
enginePath = os.path.join(
settings.RUNNER_DIR,
"pico8/web/engines",
self.runner_config.get("engine") + ".js",
)
if not os.path.exists(enginePath):
downloadUrl = ("https://www.lexaloffle.com/bbs/" + self.runner_config.get("engine") + ".js")
system.create_folder(os.path.dirname(enginePath))
downloadCompleted = False
def on_downloaded_engine():
nonlocal downloadCompleted
downloadCompleted = True
dl = Downloader(downloadUrl, enginePath, True, callback=on_downloaded_engine)
dl.start()
dl.thread.join() # Doesn't actually wait until finished
# Waits for download to complete
while not os.path.exists(enginePath):
if downloadCompleted or dl.state == Downloader.ERROR:
logger.error("Could not download engine from %s", downloadUrl)
return False
sleep(0.1)
return True
redream
¶
redream (Runner)
¶
Source code in lutris/runners/redream.py
class redream(Runner):
human_name = _("Redream")
description = _("Sega Dreamcast emulator")
platforms = [_("Sega Dreamcast")]
runner_executable = "redream/redream"
download_url = "https://redream.io/download/redream.x86_64-linux-v1.5.0.tar.gz"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("Disc image file"),
"help": _("Game data file\n" "Supported formats: GDI, CDI, CHD"),
}
]
runner_options = [
{"option": "fs", "type": "bool", "label": _("Fullscreen"), "default": False},
{
"option": "ar",
"type": "choice",
"label": _("Aspect Ratio"),
"choices": [(_("4:3"), "4:3"), (_("Stretch"), "stretch")],
"default": "4:3",
},
{
"option": "region",
"type": "choice",
"label": _("Region"),
"choices": [(_("USA"), "usa"), (_("Europe"), "europe"), (_("Japan"), "japan")],
"default": "usa",
},
{
"option": "language",
"type": "choice",
"label": _("System Language"),
"choices": [
(_("English"), "english"),
(_("German"), "german"),
(_("French"), "french"),
(_("Spanish"), "spanish"),
(_("Italian"), "italian"),
(_("Japanese"), "japanese"),
],
"default": "english",
},
{
"option": "broadcast",
"type": "choice",
"label": "Television System",
"choices": [
(_("NTSC"), "ntsc"),
(_("PAL"), "pal"),
(_("PAL-M (Brazil)"), "pal_m"),
(_("PAL-N (Argentina, Paraguay, Uruguay)"), "pal_n"),
],
"default": "ntsc",
},
{
"option": "time_sync",
"type": "choice",
"label": _("Time Sync"),
"choices": [
(_("Audio and video"), "audio and video"),
(_("Audio"), "audio"),
(_("Video"), "video"),
(_("None"), "none"),
],
"default": "audio and video",
"advanced": True,
},
{
"option": "int_res",
"type": "choice",
"label": _("Internal Video Resolution Scale"),
"choices": [
("×1", "1"),
("×2", "2"),
("×3", "3"),
("×4", "4"),
("×5", "5"),
("×6", "6"),
("×7", "7"),
("×8", "8"),
],
"default": "2",
"advanced": True,
"help": _("Only available in premium version."),
},
]
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
dlg = QuestionDialog(
{
"question": _("Do you want to select a premium license file?"),
"title": _("Use premium version?"),
}
)
if dlg.result == dlg.YES:
license_dlg = FileDialog(_("Select a license file"))
license_filename = license_dlg.filename
if not license_filename:
return
shutil.copy(
license_filename, os.path.join(settings.RUNNER_DIR, "redream")
)
super().install(
version=version, downloader=downloader, callback=on_runner_installed
)
def play(self):
command = [self.get_executable()]
if self.runner_config.get("fs") is True:
command.append("--fullscreen=1")
else:
command.append("--fullscreen=0")
if self.runner_config.get("ar"):
command.append("--aspect=" + self.runner_config.get("ar"))
if self.runner_config.get("region"):
command.append("--region=" + self.runner_config.get("region"))
if self.runner_config.get("language"):
command.append("--language=" + self.runner_config.get("language"))
if self.runner_config.get("broadcast"):
command.append("--broadcast=" + self.runner_config.get("broadcast"))
if self.runner_config.get("time_sync"):
command.append("--time_sync=" + self.runner_config.get("time_sync"))
if self.runner_config.get("int_res"):
command.append("--res=" + self.runner_config.get("int_res"))
command.append(self.game_config.get("main_file"))
return {"command": command}
description
¶
download_url
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/redream.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
dlg = QuestionDialog(
{
"question": _("Do you want to select a premium license file?"),
"title": _("Use premium version?"),
}
)
if dlg.result == dlg.YES:
license_dlg = FileDialog(_("Select a license file"))
license_filename = license_dlg.filename
if not license_filename:
return
shutil.copy(
license_filename, os.path.join(settings.RUNNER_DIR, "redream")
)
super().install(
version=version, downloader=downloader, callback=on_runner_installed
)
play(self)
¶
Source code in lutris/runners/redream.py
def play(self):
command = [self.get_executable()]
if self.runner_config.get("fs") is True:
command.append("--fullscreen=1")
else:
command.append("--fullscreen=0")
if self.runner_config.get("ar"):
command.append("--aspect=" + self.runner_config.get("ar"))
if self.runner_config.get("region"):
command.append("--region=" + self.runner_config.get("region"))
if self.runner_config.get("language"):
command.append("--language=" + self.runner_config.get("language"))
if self.runner_config.get("broadcast"):
command.append("--broadcast=" + self.runner_config.get("broadcast"))
if self.runner_config.get("time_sync"):
command.append("--time_sync=" + self.runner_config.get("time_sync"))
if self.runner_config.get("int_res"):
command.append("--res=" + self.runner_config.get("int_res"))
command.append(self.game_config.get("main_file"))
return {"command": command}
reicast
¶
reicast (Runner)
¶
Source code in lutris/runners/reicast.py
class reicast(Runner):
human_name = _("Reicast")
description = _("Sega Dreamcast emulator")
platforms = [_("Sega Dreamcast")]
runner_executable = "reicast/reicast.elf"
entry_point_option = "iso"
joypads = None
game_options = [
{
"option": "iso",
"type": "file",
"label": _("Disc image file"),
"help": _("The game data.\n"
"Supported formats: ISO, CDI"),
}
]
def __init__(self, config=None):
super().__init__(config)
self.runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "device_id_1",
"type": "choice",
"label": _("Gamepad 1"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_2",
"type": "choice",
"label": _("Gamepad 2"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_3",
"type": "choice",
"label": _("Gamepad 3"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_4",
"type": "choice",
"label": _("Gamepad 4"),
"choices": self.get_joypads,
"default": "-1",
},
]
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
mapping_path = system.create_folder("~/.reicast/mappings")
mapping_source = os.path.join(settings.RUNNER_DIR, "reicast/mappings")
for mapping_file in os.listdir(mapping_source):
shutil.copy(os.path.join(mapping_source, mapping_file), mapping_path)
system.create_folder("~/.reicast/data")
NoticeDialog(_("You have to copy valid BIOS files to ~/.reicast/data before playing"))
super().install(version, downloader, on_runner_installed)
def get_joypads(self):
"""Return list of joypad in a format usable in the options"""
if self.joypads:
return self.joypads
joypad_list = [("No joystick", "-1")]
joypad_devices = joypad.get_joypads()
name_counter = Counter([j[1] for j in joypad_devices])
name_indexes = {}
for (dev, joy_name) in joypad_devices:
dev_id = re.findall(r"(\d+)", dev)[0]
if name_counter[joy_name] > 1:
if joy_name not in name_indexes:
index = 1
else:
index = name_indexes[joy_name] + 1
name_indexes[joy_name] = index
else:
index = 0
if index:
joy_name += " (%d)" % index
joypad_list.append((joy_name, dev_id))
self.joypads = joypad_list
return joypad_list
@staticmethod
def write_config(config):
# use RawConfigParser to preserve case-sensitive configs written by Reicast
# otherwise, Reicast will write with mixed-case and Lutris will overwrite with all lowercase
# which will confuse Reicast
parser = RawConfigParser()
parser.optionxform = lambda option: option
config_path = os.path.expanduser("~/.reicast/emu.cfg")
if system.path_exists(config_path):
with open(config_path, "r", encoding='utf-8') as config_file:
parser.read_file(config_file)
for section in config:
if not parser.has_section(section):
parser.add_section(section)
for (key, value) in config[section].items():
parser.set(section, key, str(value))
with open(config_path, "w", encoding='utf-8') as config_file:
parser.write(config_file)
def play(self):
fullscreen = "1" if self.runner_config.get("fullscreen") else "0"
reicast_config = {
"x11": {
"fullscreen": fullscreen
},
"input": {},
"players": {
"nb": "1"
},
}
players = 1
reicast_config["input"] = {}
for index in range(1, 5):
config_string = "device_id_%d" % index
joy_id = self.runner_config.get(config_string) or "-1"
reicast_config["input"]["evdev_{}".format(config_string)] = joy_id
if index > 1 and joy_id != "-1":
players += 1
reicast_config["players"]["nb"] = players
self.write_config(reicast_config)
iso = self.game_config.get("iso")
command = [self.get_executable(), "-config", "config:image={}".format(iso)]
return {"command": command}
description
¶
entry_point_option
¶
game_options
¶
human_name
¶
joypads
¶
platforms
¶
runner_executable
¶
__init__(self, config=None)
special
¶
Source code in lutris/runners/reicast.py
def __init__(self, config=None):
super().__init__(config)
self.runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "device_id_1",
"type": "choice",
"label": _("Gamepad 1"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_2",
"type": "choice",
"label": _("Gamepad 2"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_3",
"type": "choice",
"label": _("Gamepad 3"),
"choices": self.get_joypads,
"default": "-1",
},
{
"option": "device_id_4",
"type": "choice",
"label": _("Gamepad 4"),
"choices": self.get_joypads,
"default": "-1",
},
]
get_joypads(self)
¶
Return list of joypad in a format usable in the options
Source code in lutris/runners/reicast.py
def get_joypads(self):
"""Return list of joypad in a format usable in the options"""
if self.joypads:
return self.joypads
joypad_list = [("No joystick", "-1")]
joypad_devices = joypad.get_joypads()
name_counter = Counter([j[1] for j in joypad_devices])
name_indexes = {}
for (dev, joy_name) in joypad_devices:
dev_id = re.findall(r"(\d+)", dev)[0]
if name_counter[joy_name] > 1:
if joy_name not in name_indexes:
index = 1
else:
index = name_indexes[joy_name] + 1
name_indexes[joy_name] = index
else:
index = 0
if index:
joy_name += " (%d)" % index
joypad_list.append((joy_name, dev_id))
self.joypads = joypad_list
return joypad_list
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/reicast.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
mapping_path = system.create_folder("~/.reicast/mappings")
mapping_source = os.path.join(settings.RUNNER_DIR, "reicast/mappings")
for mapping_file in os.listdir(mapping_source):
shutil.copy(os.path.join(mapping_source, mapping_file), mapping_path)
system.create_folder("~/.reicast/data")
NoticeDialog(_("You have to copy valid BIOS files to ~/.reicast/data before playing"))
super().install(version, downloader, on_runner_installed)
play(self)
¶
Source code in lutris/runners/reicast.py
def play(self):
fullscreen = "1" if self.runner_config.get("fullscreen") else "0"
reicast_config = {
"x11": {
"fullscreen": fullscreen
},
"input": {},
"players": {
"nb": "1"
},
}
players = 1
reicast_config["input"] = {}
for index in range(1, 5):
config_string = "device_id_%d" % index
joy_id = self.runner_config.get(config_string) or "-1"
reicast_config["input"]["evdev_{}".format(config_string)] = joy_id
if index > 1 and joy_id != "-1":
players += 1
reicast_config["players"]["nb"] = players
self.write_config(reicast_config)
iso = self.game_config.get("iso")
command = [self.get_executable(), "-config", "config:image={}".format(iso)]
return {"command": command}
write_config(config)
staticmethod
¶
Source code in lutris/runners/reicast.py
@staticmethod
def write_config(config):
# use RawConfigParser to preserve case-sensitive configs written by Reicast
# otherwise, Reicast will write with mixed-case and Lutris will overwrite with all lowercase
# which will confuse Reicast
parser = RawConfigParser()
parser.optionxform = lambda option: option
config_path = os.path.expanduser("~/.reicast/emu.cfg")
if system.path_exists(config_path):
with open(config_path, "r", encoding='utf-8') as config_file:
parser.read_file(config_file)
for section in config:
if not parser.has_section(section):
parser.add_section(section)
for (key, value) in config[section].items():
parser.set(section, key, str(value))
with open(config_path, "w", encoding='utf-8') as config_file:
parser.write(config_file)
residualvm
¶
ResidualVM runner
RESIDUALVM_CONFIG_FILE
¶
residualvm (Runner)
¶
Source code in lutris/runners/residualvm.py
class residualvm(Runner):
human_name = _("ResidualVM")
platforms = [_("Linux")] # TODO
description = _("3D point-and-click adventure games engine")
runner_executable = "residualvm/residualvm"
game_options = [
{
"option": "game_id",
"type": "string",
"label": _("Game identifier")
},
{
"option": "path",
"type": "directory_chooser",
"label": _("Game files location")
},
{
"option": "subtitles",
"label": _("Enable subtitles (if the game has voice)"),
"type": "bool",
"default": False,
},
]
runner_options = [
{
"option": "fullscreen",
"label": _("Fullscreen"),
"type": "bool",
"default": False,
},
{
"option": "renderer",
"label": _("Renderer"),
"type": "choice",
"choices": (
("OpenGL", "opengl"),
(_("OpenGL shaders"), "opengl_shaders"),
(_("Software"), "software"),
),
"default": "opengl",
},
{
"option": "show-fps",
"label": _("Display FPS information"),
"type": "bool",
"default": False,
},
]
@property
def game_path(self):
return self.game_config.get("path")
def get_residualvm_data_dir(self):
root_dir = os.path.dirname(self.get_executable())
return os.path.join(root_dir, "data")
def play(self):
command = [
self.get_executable(),
"--extrapath=%s" % self.get_residualvm_data_dir(),
"--themepath=%s" % self.get_residualvm_data_dir(),
]
# Options
if self.game_config.get("subtitles"):
command.append("--subtitles")
if self.runner_config.get("fullscreen"):
command.append("--fullscreen")
else:
command.append("--no-fullscreen")
renderer = self.runner_config.get("renderer")
if renderer:
command.append("--renderer=%s" % renderer)
if self.runner_config.get("show-fps"):
command.append("--show-fps")
else:
command.append("--no-show-fps")
# /Options
command.append("--path=%s" % self.game_path)
command.append(self.game_config.get("game_id"))
launch_info = {"command": command}
return launch_info
def get_game_list(self):
"""Return the entire list of games supported by ResidualVM."""
with subprocess.Popen([self.get_executable(), "--list-games"],
stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as residualvm_process:
residual_output = residualvm_process.communicate()[0]
game_list = str.split(residual_output, "\n")
game_array = []
game_list_start = False
for game in game_list:
if game_list_start:
if len(game) > 1:
dir_limit = game.index(" ")
else:
dir_limit = None
if dir_limit is not None:
game_dir = game[0:dir_limit]
game_name = game[dir_limit + 1:len(game)].strip()
game_array.append([game_dir, game_name])
# The actual list is below a separator
if game.startswith("-----"):
game_list_start = True
return game_array
description
¶
game_options
¶
game_path
property
readonly
¶
Return the directory where the game is installed.
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
get_game_list(self)
¶
Return the entire list of games supported by ResidualVM.
Source code in lutris/runners/residualvm.py
def get_game_list(self):
"""Return the entire list of games supported by ResidualVM."""
with subprocess.Popen([self.get_executable(), "--list-games"],
stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as residualvm_process:
residual_output = residualvm_process.communicate()[0]
game_list = str.split(residual_output, "\n")
game_array = []
game_list_start = False
for game in game_list:
if game_list_start:
if len(game) > 1:
dir_limit = game.index(" ")
else:
dir_limit = None
if dir_limit is not None:
game_dir = game[0:dir_limit]
game_name = game[dir_limit + 1:len(game)].strip()
game_array.append([game_dir, game_name])
# The actual list is below a separator
if game.startswith("-----"):
game_list_start = True
return game_array
get_residualvm_data_dir(self)
¶
Source code in lutris/runners/residualvm.py
def get_residualvm_data_dir(self):
root_dir = os.path.dirname(self.get_executable())
return os.path.join(root_dir, "data")
play(self)
¶
Source code in lutris/runners/residualvm.py
def play(self):
command = [
self.get_executable(),
"--extrapath=%s" % self.get_residualvm_data_dir(),
"--themepath=%s" % self.get_residualvm_data_dir(),
]
# Options
if self.game_config.get("subtitles"):
command.append("--subtitles")
if self.runner_config.get("fullscreen"):
command.append("--fullscreen")
else:
command.append("--no-fullscreen")
renderer = self.runner_config.get("renderer")
if renderer:
command.append("--renderer=%s" % renderer)
if self.runner_config.get("show-fps"):
command.append("--show-fps")
else:
command.append("--no-show-fps")
# /Options
command.append("--path=%s" % self.game_path)
command.append(self.game_config.get("game_id"))
launch_info = {"command": command}
return launch_info
rpcs3
¶
rpcs3 (Runner)
¶
Source code in lutris/runners/rpcs3.py
class rpcs3(Runner):
human_name = _("RPCS3")
description = _("PlayStation 3 emulator")
platforms = [_("Sony PlayStation 3")]
runnable_alone = True
runner_executable = "rpcs3/rpcs3"
game_options = [
{
"option": "main_file",
"type": "file",
"default_path": "game_path",
"label": _("Path to EBOOT.BIN"),
}
]
runner_options = [{"option": "nogui", "type": "bool", "label": _("No GUI"), "default": False}]
# RPCS3 currently uses an AppImage, no need for the runtime.
system_options_override = [{"option": "disable_runtime", "default": True}]
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("nogui"):
arguments.append("--no-gui")
eboot = self.game_config.get("main_file") or ""
if not system.path_exists(eboot):
return {"error": "FILE_NOT_FOUND", "file": eboot}
arguments.append(eboot)
return {"command": arguments}
description
¶
game_options
¶
human_name
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
system_options_override
¶
play(self)
¶
Source code in lutris/runners/rpcs3.py
def play(self):
arguments = [self.get_executable()]
if self.runner_config.get("nogui"):
arguments.append("--no-gui")
eboot = self.game_config.get("main_file") or ""
if not system.path_exists(eboot):
return {"error": "FILE_NOT_FOUND", "file": eboot}
arguments.append(eboot)
return {"command": arguments}
runner
¶
Base module for runners
Runner
¶
Generic runner (base class for other runners).
Source code in lutris/runners/runner.py
class Runner: # pylint: disable=too-many-public-methods
"""Generic runner (base class for other runners)."""
multiple_versions = False
platforms = []
runnable_alone = False
game_options = []
runner_options = []
system_options_override = []
context_menu_entries = []
require_libs = []
runner_executable = None
entry_point_option = "main_file"
download_url = None
arch = None # If the runner is only available for an architecture that isn't x86_64
def __init__(self, config=None):
"""Initialize runner."""
self.config = config
if config:
self.game_data = get_game_by_field(self.config.game_config_id, "configpath")
else:
self.game_data = {}
def __lt__(self, other):
return self.name < other.name
@property
def description(self):
"""Return the class' docstring as the description."""
return self.__doc__
@description.setter
def description(self, value):
"""Leave the ability to override the docstring."""
self.__doc__ = value # What the shit
@property
def name(self):
return self.__class__.__name__
@property
def default_config(self):
return LutrisConfig(runner_slug=self.name)
@property
def game_config(self):
"""Return the cascaded game config as a dict."""
return self.config.game_config if self.config else {}
@property
def runner_config(self):
"""Return the cascaded runner config as a dict."""
if self.config:
return self.config.runner_config
return self.default_config.runner_config
@property
def system_config(self):
"""Return the cascaded system config as a dict."""
if self.config:
return self.config.system_config
return self.default_config.system_config
@property
def default_path(self):
"""Return the default path where games are installed."""
return self.system_config.get("game_path")
@property
def game_path(self):
"""Return the directory where the game is installed."""
game_path = self.game_data.get("directory")
if game_path:
return game_path
# Default to the directory where the entry point is located.
entry_point = self.game_config.get(self.entry_point_option)
if entry_point:
return os.path.dirname(os.path.expanduser(entry_point))
return ""
@property
def library_folders(self):
"""Return a list of paths where a game might be installed"""
return []
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
return self.game_path or os.path.expanduser("~/")
@property
def shader_cache_dir(self):
"""Return the cache directory for this runner to use. We create
this if it does not exist."""
path = os.path.join(settings.SHADER_CACHE_DIR, self.name)
if not os.path.isdir(path):
os.mkdir(path)
return path
@property
def nvidia_shader_cache_path(self):
"""The path to place in __GL_SHADER_DISK_CACHE_PATH; NVidia
will place its cache cache in a subdirectory here."""
return self.shader_cache_dir
@property
def discord_client_id(self):
if self.game_data.get("discord_client_id"):
return self.game_data.get("discord_client_id")
def get_platform(self):
return self.platforms[0]
def get_runner_options(self):
runner_options = self.runner_options[:]
if self.runner_executable:
runner_options.append(
{
"option": "runner_executable",
"type": "file",
"label": _("Custom executable for the runner"),
"advanced": True,
}
)
return runner_options
def get_executable(self):
if "runner_executable" in self.runner_config:
runner_executable = self.runner_config["runner_executable"]
if os.path.isfile(runner_executable):
return runner_executable
if not self.runner_executable:
raise ValueError("runner_executable not set for {}".format(self.name))
return os.path.join(settings.RUNNER_DIR, self.runner_executable)
def get_env(self, os_env=False):
"""Return environment variables used for a game."""
env = {}
if os_env:
env.update(os.environ.copy())
# By default we'll set NVidia's shader disk cache to be
# per-game, so it overflows less readily.
env["__GL_SHADER_DISK_CACHE"] = "1"
env["__GL_SHADER_DISK_CACHE_PATH"] = self.nvidia_shader_cache_path
# Override SDL2 controller configuration
sdl_gamecontrollerconfig = self.system_config.get("sdl_gamecontrollerconfig")
if sdl_gamecontrollerconfig:
path = os.path.expanduser(sdl_gamecontrollerconfig)
if system.path_exists(path):
with open(path, "r", encoding='utf-8') as controllerdb_file:
sdl_gamecontrollerconfig = controllerdb_file.read()
env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig
# Set monitor to use for SDL 1 games
sdl_video_fullscreen = self.system_config.get("sdl_video_fullscreen")
if sdl_video_fullscreen and sdl_video_fullscreen != "off":
env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen
# DRI Prime
if self.system_config.get("dri_prime"):
env["DRI_PRIME"] = "1"
# Prime vars
prime = self.system_config.get("prime")
if prime:
env["__NV_PRIME_RENDER_OFFLOAD"] = "1"
env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia"
env["__VK_LAYER_NV_optimus"] = "NVIDIA_only"
# Set PulseAudio latency to 60ms
if self.system_config.get("pulse_latency"):
env["PULSE_LATENCY_MSEC"] = "60"
# Vulkan ICD files
vk_icd = self.system_config.get("vk_icd")
if vk_icd:
env["VK_ICD_FILENAMES"] = vk_icd
runtime_ld_library_path = None
if self.use_runtime():
runtime_env = self.get_runtime_env()
runtime_ld_library_path = runtime_env.get("LD_LIBRARY_PATH")
if runtime_ld_library_path:
ld_library_path = env.get("LD_LIBRARY_PATH")
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
runtime_ld_library_path, ld_library_path]))
# Apply user overrides at the end
env.update(self.system_config.get("env") or {})
return env
def get_runtime_env(self):
"""Return runtime environment variables.
This method may be overridden in runner classes.
(Notably for Lutris wine builds)
Returns:
dict
"""
return runtime.get_env(prefer_system_libs=self.system_config.get("prefer_system_libs", True))
def prelaunch(self):
"""Run actions before running the game, override this method in runners"""
available_libs = set()
for lib in set(self.require_libs):
if lib in LINUX_SYSTEM.shared_libraries:
if self.arch:
if self.arch in [_lib.arch for _lib in LINUX_SYSTEM.shared_libraries[lib]]:
available_libs.add(lib)
else:
available_libs.add(lib)
unavailable_libs = set(self.require_libs) - available_libs
if unavailable_libs:
raise UnavailableLibraries(unavailable_libs, self.arch)
return True
def get_run_data(self):
"""Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be."""
return {"command": [self.get_executable()], "env": self.get_env()}
def run(self, *args):
"""Run the runner alone."""
if not self.runnable_alone:
return
if not self.is_installed():
if not self.install_dialog():
logger.info("Runner install cancelled")
return
command_data = self.get_run_data()
command = command_data.get("command")
env = (command_data.get("env") or {}).copy()
if hasattr(self, "prelaunch"):
self.prelaunch()
command_runner = MonitoredCommand(command, runner=self, env=env)
command_runner.start()
def use_runtime(self):
if runtime.RUNTIME_DISABLED:
logger.info("Runtime disabled by environment")
return False
if self.system_config.get("disable_runtime"):
logger.info("Runtime disabled by system configuration")
return False
return True
def install_dialog(self):
"""Ask the user if they want to install the runner.
Return success of runner installation.
"""
dialog = dialogs.QuestionDialog(
{
"question": _("The required runner is not installed.\n"
"Do you wish to install it now?"),
"title": _("Required runner unavailable"),
}
)
if Gtk.ResponseType.YES == dialog.result:
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.dialogs.download import simple_downloader
try:
if hasattr(self, "get_version"):
version = self.get_version(use_default=False) # pylint: disable=no-member
self.install(downloader=simple_downloader, version=version)
else:
self.install(downloader=simple_downloader)
except RunnerInstallationError as ex:
ErrorDialog(ex.message)
return self.is_installed()
return False
def is_installed(self):
"""Return whether the runner is installed"""
return system.path_exists(self.get_executable())
def get_runner_version(self, version=None):
"""Get the appropriate version for a runner
Params:
version (str): Optional version to lookup, will return this one if found
Returns:
dict: Dict containing version, architecture and url for the runner, None
if the data can't be retrieved.
"""
logger.info(
"Getting runner information for %s%s",
self.name,
" (version: %s)" % version if version else "",
)
try:
request = Request("{}/api/runners/{}".format(settings.SITE_URL, self.name))
runner_info = request.get().json
if not runner_info:
logger.error("Failed to get runner information")
except HTTPError as ex:
logger.error("Unable to get runner information: %s", ex)
runner_info = None
if not runner_info:
return
versions = runner_info.get("versions") or []
arch = LINUX_SYSTEM.arch
if version:
if version.endswith("-i386") or version.endswith("-x86_64"):
version, arch = version.rsplit("-", 1)
versions = [v for v in versions if v["version"] == version]
versions_for_arch = [v for v in versions if v["architecture"] == arch]
if len(versions_for_arch) == 1:
return versions_for_arch[0]
if len(versions_for_arch) > 1:
default_version = [v for v in versions_for_arch if v["default"] is True]
if default_version:
return default_version[0]
elif len(versions) == 1 and LINUX_SYSTEM.is_64_bit:
return versions[0]
elif len(versions) > 1 and LINUX_SYSTEM.is_64_bit:
default_version = [v for v in versions if v["default"] is True]
if default_version:
return default_version[0]
# If we didn't find a proper version yet, return the first available.
if len(versions_for_arch) >= 1:
return versions_for_arch[0]
def install(self, version=None, downloader=None, callback=None):
"""Install runner using package management systems."""
logger.debug(
"Installing %s (version=%s, downloader=%s, callback=%s)",
self.name,
version,
downloader,
callback,
)
opts = {"downloader": downloader, "callback": callback}
if self.download_url:
opts["dest"] = os.path.join(settings.RUNNER_DIR, self.name)
return self.download_and_extract(self.download_url, **opts)
runner = self.get_runner_version(version)
if not runner:
raise RunnerInstallationError("Failed to retrieve {} ({}) information".format(self.name, version))
if not downloader:
raise RuntimeError("Missing mandatory downloader for runner %s" % self)
if "wine" in self.name:
opts["merge_single"] = True
opts["dest"] = os.path.join(
settings.RUNNER_DIR, self.name, "{}-{}".format(runner["version"], runner["architecture"])
)
if self.name == "libretro" and version:
opts["merge_single"] = False
opts["dest"] = os.path.join(settings.RUNNER_DIR, "retroarch/cores")
self.download_and_extract(runner["url"], **opts)
def download_and_extract(self, url, dest=None, **opts):
downloader = opts["downloader"]
merge_single = opts.get("merge_single", False)
callback = opts.get("callback")
tarball_filename = os.path.basename(url)
runner_archive = os.path.join(settings.CACHE_DIR, tarball_filename)
if not dest:
dest = settings.RUNNER_DIR
downloader(
url, runner_archive, self.extract, {
"archive": runner_archive,
"dest": dest,
"merge_single": merge_single,
"callback": callback,
}
)
def extract(self, archive=None, dest=None, merge_single=None, callback=None):
if not system.path_exists(archive):
raise RunnerInstallationError("Failed to extract {}".format(archive))
try:
extract_archive(archive, dest, merge_single=merge_single)
except ExtractFailure as ex:
logger.error("Failed to extract the archive %s file may be corrupt", archive)
raise RunnerInstallationError("Failed to extract {}: {}".format(archive, ex)) from ex
os.remove(archive)
if self.name == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
if self.runner_executable:
runner_executable = os.path.join(settings.RUNNER_DIR, self.runner_executable)
if os.path.isfile(runner_executable):
system.make_executable(runner_executable)
if callback:
callback()
@staticmethod
def remove_game_data(app_id=None, game_path=None):
system.remove_folder(game_path)
def can_uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
return os.path.isdir(runner_path)
def uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
if os.path.isdir(runner_path):
system.remove_folder(runner_path)
def find_option(self, options_group, option_name):
"""Retrieve an option dict if it exists in the group"""
if options_group not in ['game_options', 'runner_options']:
return None
output = None
for item in getattr(self, options_group):
if item["option"] == option_name:
output = item
break
return output
def force_stop_game(self, game):
"""Stop the running game. If this leaves any game processes running,
the caller will SIGKILL them (after a delay)."""
game.kill_processes(signal.SIGTERM)
arch
¶
context_menu_entries
¶
default_config
property
readonly
¶
default_path
property
readonly
¶
Return the default path where games are installed.
description
property
writable
¶
Return the class' docstring as the description.
discord_client_id
property
readonly
¶
download_url
¶
entry_point_option
¶
game_config
property
readonly
¶
Return the cascaded game config as a dict.
game_options
¶
game_path
property
readonly
¶
Return the directory where the game is installed.
library_folders
property
readonly
¶
Return a list of paths where a game might be installed
multiple_versions
¶
name
property
readonly
¶
nvidia_shader_cache_path
property
readonly
¶
The path to place in __GL_SHADER_DISK_CACHE_PATH; NVidia will place its cache cache in a subdirectory here.
platforms
¶
require_libs
¶
runnable_alone
¶
runner_config
property
readonly
¶
Return the cascaded runner config as a dict.
runner_executable
¶
runner_options
¶
shader_cache_dir
property
readonly
¶
Return the cache directory for this runner to use. We create this if it does not exist.
system_config
property
readonly
¶
Return the cascaded system config as a dict.
system_options_override
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
__init__(self, config=None)
special
¶
Initialize runner.
Source code in lutris/runners/runner.py
def __init__(self, config=None):
"""Initialize runner."""
self.config = config
if config:
self.game_data = get_game_by_field(self.config.game_config_id, "configpath")
else:
self.game_data = {}
__lt__(self, other)
special
¶
Source code in lutris/runners/runner.py
def __lt__(self, other):
return self.name < other.name
can_uninstall(self)
¶
Source code in lutris/runners/runner.py
def can_uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
return os.path.isdir(runner_path)
download_and_extract(self, url, dest=None, **opts)
¶
Source code in lutris/runners/runner.py
def download_and_extract(self, url, dest=None, **opts):
downloader = opts["downloader"]
merge_single = opts.get("merge_single", False)
callback = opts.get("callback")
tarball_filename = os.path.basename(url)
runner_archive = os.path.join(settings.CACHE_DIR, tarball_filename)
if not dest:
dest = settings.RUNNER_DIR
downloader(
url, runner_archive, self.extract, {
"archive": runner_archive,
"dest": dest,
"merge_single": merge_single,
"callback": callback,
}
)
extract(self, archive=None, dest=None, merge_single=None, callback=None)
¶
Source code in lutris/runners/runner.py
def extract(self, archive=None, dest=None, merge_single=None, callback=None):
if not system.path_exists(archive):
raise RunnerInstallationError("Failed to extract {}".format(archive))
try:
extract_archive(archive, dest, merge_single=merge_single)
except ExtractFailure as ex:
logger.error("Failed to extract the archive %s file may be corrupt", archive)
raise RunnerInstallationError("Failed to extract {}: {}".format(archive, ex)) from ex
os.remove(archive)
if self.name == "wine":
logger.debug("Clearing wine version cache")
from lutris.util.wine.wine import get_wine_versions
get_wine_versions.cache_clear()
if self.runner_executable:
runner_executable = os.path.join(settings.RUNNER_DIR, self.runner_executable)
if os.path.isfile(runner_executable):
system.make_executable(runner_executable)
if callback:
callback()
find_option(self, options_group, option_name)
¶
Retrieve an option dict if it exists in the group
Source code in lutris/runners/runner.py
def find_option(self, options_group, option_name):
"""Retrieve an option dict if it exists in the group"""
if options_group not in ['game_options', 'runner_options']:
return None
output = None
for item in getattr(self, options_group):
if item["option"] == option_name:
output = item
break
return output
force_stop_game(self, game)
¶
Stop the running game. If this leaves any game processes running, the caller will SIGKILL them (after a delay).
Source code in lutris/runners/runner.py
def force_stop_game(self, game):
"""Stop the running game. If this leaves any game processes running,
the caller will SIGKILL them (after a delay)."""
game.kill_processes(signal.SIGTERM)
get_env(self, os_env=False)
¶
Return environment variables used for a game.
Source code in lutris/runners/runner.py
def get_env(self, os_env=False):
"""Return environment variables used for a game."""
env = {}
if os_env:
env.update(os.environ.copy())
# By default we'll set NVidia's shader disk cache to be
# per-game, so it overflows less readily.
env["__GL_SHADER_DISK_CACHE"] = "1"
env["__GL_SHADER_DISK_CACHE_PATH"] = self.nvidia_shader_cache_path
# Override SDL2 controller configuration
sdl_gamecontrollerconfig = self.system_config.get("sdl_gamecontrollerconfig")
if sdl_gamecontrollerconfig:
path = os.path.expanduser(sdl_gamecontrollerconfig)
if system.path_exists(path):
with open(path, "r", encoding='utf-8') as controllerdb_file:
sdl_gamecontrollerconfig = controllerdb_file.read()
env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig
# Set monitor to use for SDL 1 games
sdl_video_fullscreen = self.system_config.get("sdl_video_fullscreen")
if sdl_video_fullscreen and sdl_video_fullscreen != "off":
env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen
# DRI Prime
if self.system_config.get("dri_prime"):
env["DRI_PRIME"] = "1"
# Prime vars
prime = self.system_config.get("prime")
if prime:
env["__NV_PRIME_RENDER_OFFLOAD"] = "1"
env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia"
env["__VK_LAYER_NV_optimus"] = "NVIDIA_only"
# Set PulseAudio latency to 60ms
if self.system_config.get("pulse_latency"):
env["PULSE_LATENCY_MSEC"] = "60"
# Vulkan ICD files
vk_icd = self.system_config.get("vk_icd")
if vk_icd:
env["VK_ICD_FILENAMES"] = vk_icd
runtime_ld_library_path = None
if self.use_runtime():
runtime_env = self.get_runtime_env()
runtime_ld_library_path = runtime_env.get("LD_LIBRARY_PATH")
if runtime_ld_library_path:
ld_library_path = env.get("LD_LIBRARY_PATH")
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
runtime_ld_library_path, ld_library_path]))
# Apply user overrides at the end
env.update(self.system_config.get("env") or {})
return env
get_executable(self)
¶
Source code in lutris/runners/runner.py
def get_executable(self):
if "runner_executable" in self.runner_config:
runner_executable = self.runner_config["runner_executable"]
if os.path.isfile(runner_executable):
return runner_executable
if not self.runner_executable:
raise ValueError("runner_executable not set for {}".format(self.name))
return os.path.join(settings.RUNNER_DIR, self.runner_executable)
get_platform(self)
¶
Source code in lutris/runners/runner.py
def get_platform(self):
return self.platforms[0]
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/runner.py
def get_run_data(self):
"""Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be."""
return {"command": [self.get_executable()], "env": self.get_env()}
get_runner_options(self)
¶
Source code in lutris/runners/runner.py
def get_runner_options(self):
runner_options = self.runner_options[:]
if self.runner_executable:
runner_options.append(
{
"option": "runner_executable",
"type": "file",
"label": _("Custom executable for the runner"),
"advanced": True,
}
)
return runner_options
get_runner_version(self, version=None)
¶
Get the appropriate version for a runner
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
version |
str |
Optional version to lookup, will return this one if found |
None |
Returns:
| Type | Description |
|---|---|
dict |
Dict containing version, architecture and url for the runner, None if the data can't be retrieved. |
Source code in lutris/runners/runner.py
def get_runner_version(self, version=None):
"""Get the appropriate version for a runner
Params:
version (str): Optional version to lookup, will return this one if found
Returns:
dict: Dict containing version, architecture and url for the runner, None
if the data can't be retrieved.
"""
logger.info(
"Getting runner information for %s%s",
self.name,
" (version: %s)" % version if version else "",
)
try:
request = Request("{}/api/runners/{}".format(settings.SITE_URL, self.name))
runner_info = request.get().json
if not runner_info:
logger.error("Failed to get runner information")
except HTTPError as ex:
logger.error("Unable to get runner information: %s", ex)
runner_info = None
if not runner_info:
return
versions = runner_info.get("versions") or []
arch = LINUX_SYSTEM.arch
if version:
if version.endswith("-i386") or version.endswith("-x86_64"):
version, arch = version.rsplit("-", 1)
versions = [v for v in versions if v["version"] == version]
versions_for_arch = [v for v in versions if v["architecture"] == arch]
if len(versions_for_arch) == 1:
return versions_for_arch[0]
if len(versions_for_arch) > 1:
default_version = [v for v in versions_for_arch if v["default"] is True]
if default_version:
return default_version[0]
elif len(versions) == 1 and LINUX_SYSTEM.is_64_bit:
return versions[0]
elif len(versions) > 1 and LINUX_SYSTEM.is_64_bit:
default_version = [v for v in versions if v["default"] is True]
if default_version:
return default_version[0]
# If we didn't find a proper version yet, return the first available.
if len(versions_for_arch) >= 1:
return versions_for_arch[0]
get_runtime_env(self)
¶
Return runtime environment variables.
This method may be overridden in runner classes. (Notably for Lutris wine builds)
Returns:
| Type | Description |
|---|---|
dict |
Source code in lutris/runners/runner.py
def get_runtime_env(self):
"""Return runtime environment variables.
This method may be overridden in runner classes.
(Notably for Lutris wine builds)
Returns:
dict
"""
return runtime.get_env(prefer_system_libs=self.system_config.get("prefer_system_libs", True))
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/runner.py
def install(self, version=None, downloader=None, callback=None):
"""Install runner using package management systems."""
logger.debug(
"Installing %s (version=%s, downloader=%s, callback=%s)",
self.name,
version,
downloader,
callback,
)
opts = {"downloader": downloader, "callback": callback}
if self.download_url:
opts["dest"] = os.path.join(settings.RUNNER_DIR, self.name)
return self.download_and_extract(self.download_url, **opts)
runner = self.get_runner_version(version)
if not runner:
raise RunnerInstallationError("Failed to retrieve {} ({}) information".format(self.name, version))
if not downloader:
raise RuntimeError("Missing mandatory downloader for runner %s" % self)
if "wine" in self.name:
opts["merge_single"] = True
opts["dest"] = os.path.join(
settings.RUNNER_DIR, self.name, "{}-{}".format(runner["version"], runner["architecture"])
)
if self.name == "libretro" and version:
opts["merge_single"] = False
opts["dest"] = os.path.join(settings.RUNNER_DIR, "retroarch/cores")
self.download_and_extract(runner["url"], **opts)
install_dialog(self)
¶
Ask the user if they want to install the runner.
Return success of runner installation.
Source code in lutris/runners/runner.py
def install_dialog(self):
"""Ask the user if they want to install the runner.
Return success of runner installation.
"""
dialog = dialogs.QuestionDialog(
{
"question": _("The required runner is not installed.\n"
"Do you wish to install it now?"),
"title": _("Required runner unavailable"),
}
)
if Gtk.ResponseType.YES == dialog.result:
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.dialogs.download import simple_downloader
try:
if hasattr(self, "get_version"):
version = self.get_version(use_default=False) # pylint: disable=no-member
self.install(downloader=simple_downloader, version=version)
else:
self.install(downloader=simple_downloader)
except RunnerInstallationError as ex:
ErrorDialog(ex.message)
return self.is_installed()
return False
is_installed(self)
¶
Return whether the runner is installed
Source code in lutris/runners/runner.py
def is_installed(self):
"""Return whether the runner is installed"""
return system.path_exists(self.get_executable())
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/runner.py
def prelaunch(self):
"""Run actions before running the game, override this method in runners"""
available_libs = set()
for lib in set(self.require_libs):
if lib in LINUX_SYSTEM.shared_libraries:
if self.arch:
if self.arch in [_lib.arch for _lib in LINUX_SYSTEM.shared_libraries[lib]]:
available_libs.add(lib)
else:
available_libs.add(lib)
unavailable_libs = set(self.require_libs) - available_libs
if unavailable_libs:
raise UnavailableLibraries(unavailable_libs, self.arch)
return True
remove_game_data(app_id=None, game_path=None)
staticmethod
¶
Source code in lutris/runners/runner.py
@staticmethod
def remove_game_data(app_id=None, game_path=None):
system.remove_folder(game_path)
run(self, *args)
¶
Run the runner alone.
Source code in lutris/runners/runner.py
def run(self, *args):
"""Run the runner alone."""
if not self.runnable_alone:
return
if not self.is_installed():
if not self.install_dialog():
logger.info("Runner install cancelled")
return
command_data = self.get_run_data()
command = command_data.get("command")
env = (command_data.get("env") or {}).copy()
if hasattr(self, "prelaunch"):
self.prelaunch()
command_runner = MonitoredCommand(command, runner=self, env=env)
command_runner.start()
uninstall(self)
¶
Source code in lutris/runners/runner.py
def uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
if os.path.isdir(runner_path):
system.remove_folder(runner_path)
use_runtime(self)
¶
Source code in lutris/runners/runner.py
def use_runtime(self):
if runtime.RUNTIME_DISABLED:
logger.info("Runtime disabled by environment")
return False
if self.system_config.get("disable_runtime"):
logger.info("Runtime disabled by system configuration")
return False
return True
ryujinx
¶
ryujinx (Runner)
¶
Source code in lutris/runners/ryujinx.py
class ryujinx(Runner):
human_name = _("Ryujinx")
platforms = [_("Nintendo Switch")]
description = _("Nintendo Switch emulator")
runnable_alone = True
runner_executable = "ryujinx/publish/Ryujinx"
download_url = "https://lutris.nyc3.digitaloceanspaces.com/runners/ryujinx/ryujinx-1.0.7074-linux_x64.tar.gz"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("NSP file"),
"help": _("The game data, commonly called a ROM image."),
}
]
runner_options = [
{
"option": "prod_keys",
"label": _("Encryption keys"),
"type": "file",
"help": _("File containing the encryption keys."),
}, {
"option": "title_keys",
"label": _("Title keys"),
"type": "file",
"help": _("File containing the title keys."),
}
]
@property
def ryujinx_data_dir(self):
"""Return dir where Ryujinx files lie."""
candidates = ("~/.local/share/ryujinx", )
for candidate in candidates:
path = system.fix_path_case(os.path.join(os.path.expanduser(candidate), "nand"))
if path and system.path_exists(path):
return path[:-len("nand")]
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
def _update_key(self, key_type):
"""Update a keys file if set """
ryujinx_data_dir = self.ryujinx_data_dir
if not ryujinx_data_dir:
logger.error("Ryujinx data dir not set")
return
if key_type == "prod_keys":
key_loc = os.path.join(ryujinx_data_dir, "keys/prod.keys")
elif key_type == "title_keys":
key_loc = os.path.join(ryujinx_data_dir, "keys/title.keys")
else:
logger.error("Invalid keys type %s!", key_type)
return
key = self.runner_config.get(key_type)
if not key:
logger.debug("No %s file was set.", key_type)
return
if not system.path_exists(key):
logger.warning("Keys file %s does not exist!", key)
return
keys_dir = os.path.dirname(key_loc)
if not os.path.exists(keys_dir):
os.makedirs(keys_dir)
elif os.path.isfile(key_loc) and filecmp.cmp(key, key_loc):
# If the files are identical, don't do anything
return
copyfile(key, key_loc)
def prelaunch(self):
for key in ["prod_keys", "title_keys"]:
self._update_key(key_type=key)
return True
description
¶
download_url
¶
game_options
¶
human_name
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
ryujinx_data_dir
property
readonly
¶
Return dir where Ryujinx files lie.
play(self)
¶
Run the game.
Source code in lutris/runners/ryujinx.py
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/ryujinx.py
def prelaunch(self):
for key in ["prod_keys", "title_keys"]:
self._update_key(key_type=key)
return True
scummvm
¶
scummvm (Runner)
¶
Source code in lutris/runners/scummvm.py
class scummvm(Runner):
description = _("Engine for point-and-click games.")
human_name = _("ScummVM")
platforms = [_("Linux")]
runnable_alone = True
runner_executable = "scummvm/bin/scummvm"
game_options = [
{
"option": "game_id",
"type": "string",
"label": _("Game identifier")
},
{
"option": "path",
"type": "directory_chooser",
"label": _("Game files location")
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Command line arguments used when launching the game"),
},
]
option_map = {
"aspect": "--aspect-ratio",
"subtitles": "--subtitles",
"fullscreen": "--fullscreen",
"gfx-mode": "--gfx-mode=%s",
"scale-factor": "--scale-factor=%s",
"render-mode": "--render-mode=%s",
"filtering": "--filtering",
"platform": "--platform=%s",
"engine-speed": "--engine-speed=%s",
"talk-speed": "--talkspeed=%s",
"dimuse-tempo": "--dimuse-tempo=%s",
"music-tempo": "--tempo=%s",
"opl-driver": "--opl-driver=%s",
"output-rate": "--output-rate=%s",
"music-driver": "--music-driver=%s",
"multi-midi": "--multi-midi",
"midi-gain": "--midi-gain=%s",
"soundfont": "--soundfont=%s",
"music-volume": "--music-volume=%s",
"sfx-volume": "--sfx-volume=%s",
"speech-volume": "--speech-volume=%s",
"native-mt32": "--native-mt32",
"enable-gs": "--enable-gs",
"joystick": "--joystick=%s",
"language": "--language=%s",
"alt-intro": "--alt-intro",
"copy-protection": "--copy-protection",
"demo-mode": "--demo-mode",
"debug-level": "--debug-level=%s",
"debug-flags": "--debug-flags=%s",
}
option_empty_map = {
"fullscreen": "--no-fullscreen"
}
runner_options = [
{
"option": "fullscreen",
"label": _("Fullscreen"),
"type": "bool",
"default": True,
},
{
"option": "subtitles",
"label": _("Enable subtitles"),
"type": "bool",
"default": False,
"help": ("Enable subtitles for games with voice"),
},
{
"option": "aspect",
"label": _("Aspect ratio correction"),
"type": "bool",
"default": True,
"help": _(
"Most games supported by ScummVM were made for VGA "
"display modes using rectangular pixels. Activating "
"this option for these games will preserve the 4:3 "
"aspect ratio they were made for."
),
},
{
"option": "gfx-mode",
"label": _("Graphic scaler"),
"type": "choice",
"default": "3x",
"choices": [
("1x", "1x"),
("2x", "2x"),
("3x", "3x"),
("hq2x", "hq2x"),
("hq3x", "hq3x"),
("advmame2x", "advmame2x"),
("advmame3x", "advmame3x"),
("2xsai", "2xsai"),
("super2xsai", "super2xsai"),
("supereagle", "supereagle"),
("tv2x", "tv2x"),
("dotmatrix", "dotmatrix"),
],
"help":
_("The algorithm used to scale up the game's base "
"resolution, resulting in different visual styles. "),
},
# {
# "option": "scale-factor",
# "label": _("Scaler factor"),
# "type": "choice",
# "choices": [
# ("1", "1"),
# ("2", "2"),
# ("3", "3"),
# ("4", "4"),
# ("5", "5"),
# ],
# "help":
# _("Changes the resolution of the game. "
# "For example, a 2x scaler will take a 320x200 "
# "resolution game and scale it up to 640x400. "),
# },
{
"option": "render-mode",
"label": _("Render mode"),
"type": "choice",
"choices": [
("hercGreen", "hercGreen"),
("hercAmber", "hercAmber"),
("cga", "cga"),
("ega", "ega"),
("vga", "vga"),
("amiga", "amiga"),
("fmtowns", "fmtowns"),
("pc9821", "pc9821"),
("pc9801", "pc9801"),
("2gs", "2gs"),
("atari", "atari"),
("macintosh", "macintosh"),
],
"advanced": True,
"help": _("Changes how the game is rendered."),
},
{
"option": "filtering",
"label": _("Filtering"),
"type": "bool",
"help": _("Uses bilinear interpolation instead of nearest neighbor "
"resampling for the aspect ratio correction and stretch mode."),
"default": False,
"advanced": True,
},
{
"option": "datadir",
"label": _("Data directory"),
"type": "directory_chooser",
"help": _("Defaults to share/scummvm if unspecified."),
"advanced": True,
},
{
"option": "platform",
"type": "string",
"label": _("Platform"),
"help": _("Specifes platform of game. Allowed values: 2gs, 3do, acorn, amiga, atari, c64, "
"fmtowns, nes, mac, pc pc98, pce, segacd, wii, windows"),
"advanced": True,
},
{
"option": "joystick",
"type": "string",
"label": _("Joystick"),
"help": _("Enables joystick input (default: 0 = first joystick)"),
"advanced": True,
},
{
"option": "language",
"type": "string",
"label": _("Language"),
"help": _("Selects language (en, de, fr, it, pt, es, jp, zh, kr, se, gb, hb, ru, cz)"),
"advanced": True,
},
{
"option": "engine-speed",
"type": "string",
"label": _("Engine speed"),
"help": _("Sets frames per second limit (0 - 100) for Grim Fandango "
"or Escape from Monkey Island (default: 60)."),
"advanced": True,
},
{
"option": "talk-speed",
"type": "string",
"label": _("Talk speed"),
"help": _("Sets talk speed for games (default: 60)"),
"advanced": True,
},
{
"option": "music-tempo",
"type": "string",
"label": _("Music tempo"),
"help": _("Sets music tempo (in percent, 50-200) for SCUMM games (default: 100)"),
"advanced": True,
},
{
"option": "dimuse-tempo",
"type": "string",
"label": _("Digital iMuse tempo"),
"help": _("Sets internal Digital iMuse tempo (10 - 100) per second (default: 10)"),
"advanced": True,
},
{
"option": "music-driver",
"label": _("Music driver"),
"type": "choice",
"choices": [
("null", "null"),
("auto", "auto"),
("seq", "seq"),
("sndio", "sndio"),
("alsa", "alsa"),
("fluidsynth", "fluidsynth"),
("mt32", "mt32"),
("adlib", "adlib"),
("pcspk", "pcspk"),
("pcjr", "pcjr"),
("cms", "cms"),
("timidity", "timidity"),
],
"help": _("Specifies the device ScummVM uses to output audio."),
"advanced": True,
},
{
"option": "output-rate",
"label": _("Output rate"),
"type": "choice",
"choices": [
("11025", "11025"),
("22050", "22050"),
("44100", "44100"),
],
"help": _("Selects output sample rate in Hz."),
"advanced": True,
},
{
"option": "opl-driver",
"label": _("OPL driver"),
"type": "choice",
"choices": [
("auto", "auto"),
("mame", "mame"),
("db", "db"),
("nuked", "nuked"),
("alsa", "alsa"),
("op2lpt", "op2lpt"),
("op3lpt", "op3lpt"),
("rwopl3", "rwopl3"),
],
"help": _("Chooses which emulator is used by ScummVM when the AdLib emulator "
"is chosen as the Preferred device."),
"advanced": True,
},
{
"option": "music-volume",
"type": "string",
"label": _("Music volume"),
"help": _("Sets the music volume, 0-255 (default: 192)"),
"advanced": True,
},
{
"option": "sfx-volume",
"type": "string",
"label": _("SFX volume"),
"help": _("Sets the sfx volume, 0-255 (default: 192)"),
"advanced": True,
},
{
"option": "speech-volume",
"type": "string",
"label": _("Speech volume"),
"help": _("Sets the speech volume, 0-255 (default: 192)"),
"advanced": True,
},
{
"option": "midi-gain",
"type": "string",
"label": _("MIDI gain"),
"help": _("Sets the gain for MIDI playback. 0-1000 (default: 100)"),
"advanced": True,
},
{
"option": "soundfont",
"type": "string",
"label": _("Soundfont"),
"help": _("Specifies the path to a soundfont file."),
"advanced": True,
},
{
"option": "multi-midi",
"label": _("Mixed AdLib/MIDI mode"),
"type": "bool",
"default": False,
"help": _("Combines MIDI music with AdLib sound effects."),
"advanced": True,
},
{
"option": "native-mt32",
"label": _("True Roland MT-32"),
"type": "bool",
"default": False,
"help": _("Tells ScummVM that the MIDI device is an actual Roland MT-32, "
"LAPC-I, CM-64, CM-32L, CM-500 or other MT-32 device."),
"advanced": True,
},
{
"option": "enable-gs",
"label": _("Enable Roland GS"),
"type": "bool",
"default": False,
"help": _("Tells ScummVM that the MIDI device is a GS device that has "
"an MT-32 map, such as an SC-55, SC-88 or SC-8820."),
"advanced": True,
},
{
"option": "alt-intro",
"type": "bool",
"label": _("Use alternate intro"),
"help": _("Uses alternative intro for CD versions"),
"advanced": True,
},
{
"option": "copy-protection",
"type": "bool",
"label": _("Copy protection"),
"help": _("Enables copy protection"),
"advanced": True,
},
{
"option": "demo-mode",
"type": "bool",
"label": _("Demo mode"),
"help": _("Starts demo mode of Maniac Mansion or The 7th Guest"),
"advanced": True,
},
{
"option": "debug-level",
"type": "string",
"label": _("Debug level"),
"help": _("Sets debug verbosity level"),
"advanced": True,
},
{
"option": "debug-flags",
"type": "string",
"label": _("Debug flags"),
"help": _("Enables engine specific debug flags"),
"advanced": True,
},
]
@property
def game_path(self):
return self.game_config.get("path")
@property
def libs_dir(self):
path = os.path.join(settings.RUNNER_DIR, "scummvm/lib")
return path if system.path_exists(path) else ""
def get_command(self):
return [
self.get_executable(),
"--extrapath=%s" % self.get_scummvm_data_dir(),
"--themepath=%s" % self.get_scummvm_data_dir(),
]
def get_scummvm_data_dir(self):
data_dir = self.runner_config.get("datadir")
if data_dir is None:
root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
data_dir = os.path.join(root_dir, "share/scummvm")
return data_dir
def get_run_data(self):
env = self.get_env()
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
self.libs_dir,
env.get("LD_LIBRARY_PATH")]))
return {"env": env, "command": self.get_command()}
def inject_runner_option(self, command, key, cmdline, cmdline_empty=None):
value = self.runner_config.get(key)
if value:
if "%s" in cmdline:
command.append(cmdline % value)
else:
command.append(cmdline)
elif cmdline_empty:
command.append(cmdline_empty)
def play(self):
command = self.get_command()
for option, cmdline in self.option_map.items():
self.inject_runner_option(command, option, cmdline, self.option_empty_map.get(option))
command.append("--path=%s" % self.game_path)
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
command.append(self.game_config.get("game_id"))
return {"command": command, "ld_library_path": self.libs_dir}
def get_game_list(self):
"""Return the entire list of games supported by ScummVM."""
with subprocess.Popen([self.get_executable(), "--list-games"],
stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as scummvm_process:
scumm_output = scummvm_process.communicate()[0]
game_list = str.split(scumm_output, "\n")
game_array = []
game_list_start = False
for game in game_list:
if game_list_start:
if len(game) > 1:
dir_limit = game.index(" ")
else:
dir_limit = None
if dir_limit is not None:
game_dir = game[0:dir_limit]
game_name = game[dir_limit + 1:len(game)].strip()
game_array.append([game_dir, game_name])
# The actual list is below a separator
if game.startswith("-----"):
game_list_start = True
return game_array
description
¶
game_options
¶
game_path
property
readonly
¶
Return the directory where the game is installed.
human_name
¶
libs_dir
property
readonly
¶
option_empty_map
¶
option_map
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
get_command(self)
¶
Source code in lutris/runners/scummvm.py
def get_command(self):
return [
self.get_executable(),
"--extrapath=%s" % self.get_scummvm_data_dir(),
"--themepath=%s" % self.get_scummvm_data_dir(),
]
get_game_list(self)
¶
Return the entire list of games supported by ScummVM.
Source code in lutris/runners/scummvm.py
def get_game_list(self):
"""Return the entire list of games supported by ScummVM."""
with subprocess.Popen([self.get_executable(), "--list-games"],
stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as scummvm_process:
scumm_output = scummvm_process.communicate()[0]
game_list = str.split(scumm_output, "\n")
game_array = []
game_list_start = False
for game in game_list:
if game_list_start:
if len(game) > 1:
dir_limit = game.index(" ")
else:
dir_limit = None
if dir_limit is not None:
game_dir = game[0:dir_limit]
game_name = game[dir_limit + 1:len(game)].strip()
game_array.append([game_dir, game_name])
# The actual list is below a separator
if game.startswith("-----"):
game_list_start = True
return game_array
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/scummvm.py
def get_run_data(self):
env = self.get_env()
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
self.libs_dir,
env.get("LD_LIBRARY_PATH")]))
return {"env": env, "command": self.get_command()}
get_scummvm_data_dir(self)
¶
Source code in lutris/runners/scummvm.py
def get_scummvm_data_dir(self):
data_dir = self.runner_config.get("datadir")
if data_dir is None:
root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
data_dir = os.path.join(root_dir, "share/scummvm")
return data_dir
inject_runner_option(self, command, key, cmdline, cmdline_empty=None)
¶
Source code in lutris/runners/scummvm.py
def inject_runner_option(self, command, key, cmdline, cmdline_empty=None):
value = self.runner_config.get(key)
if value:
if "%s" in cmdline:
command.append(cmdline % value)
else:
command.append(cmdline)
elif cmdline_empty:
command.append(cmdline_empty)
play(self)
¶
Source code in lutris/runners/scummvm.py
def play(self):
command = self.get_command()
for option, cmdline in self.option_map.items():
self.inject_runner_option(command, option, cmdline, self.option_empty_map.get(option))
command.append("--path=%s" % self.game_path)
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
command.append(self.game_config.get("game_id"))
return {"command": command, "ld_library_path": self.libs_dir}
snes9x
¶
SNES9X_DIR
¶
snes9x (Runner)
¶
Source code in lutris/runners/snes9x.py
class snes9x(Runner):
description = _("Super Nintendo emulator")
human_name = _("Snes9x")
platforms = [_("Nintendo SNES")]
runnable_alone = True
runner_executable = "snes9x/bin/snes9x-gtk"
game_options = [
{
"option": "main_file",
"type": "file",
"default_path": "game_path",
"label": _("ROM file"),
"help": _("The game data, commonly called a ROM image."),
}
]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": "1"
},
{
"option":
"maintain_aspect_ratio",
"type":
"bool",
"label":
_("Maintain aspect ratio (4:3)"),
"default":
"1",
"help": _(
"Super Nintendo games were made for 4:3 "
"screens with rectangular pixels, but modern screens "
"have square pixels, which results in a vertically "
"squeezed image. This option corrects this by displaying "
"rectangular pixels."
),
},
{
"option": "sound_driver",
"type": "choice",
"label": _("Sound driver"),
"advanced": True,
"choices": (("SDL", "1"), ("ALSA", "2"), ("OSS", "0")),
"default": "1",
},
]
def set_option(self, option, value):
config_file = os.path.expanduser("~/.snes9x/snes9x.xml")
if not system.path_exists(config_file):
with subprocess.Popen([self.get_executable(), "-help"]) as snes9x_process:
snes9x_process.communicate()
if not system.path_exists(config_file):
logger.error("Snes9x config file creation failed")
return
tree = etree.parse(config_file)
node = tree.find("./preferences/option[@name='%s']" % option)
if value.__class__.__name__ == "bool":
value = "1" if value else "0"
node.attrib["value"] = value
tree.write(config_file)
def play(self):
for option_name in self.config.runner_config:
self.set_option(option_name, self.runner_config.get(option_name))
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
return {"command": [self.get_executable(), rom]}
description
¶
game_options
¶
human_name
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
play(self)
¶
Source code in lutris/runners/snes9x.py
def play(self):
for option_name in self.config.runner_config:
self.set_option(option_name, self.runner_config.get(option_name))
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
return {"command": [self.get_executable(), rom]}
set_option(self, option, value)
¶
Source code in lutris/runners/snes9x.py
def set_option(self, option, value):
config_file = os.path.expanduser("~/.snes9x/snes9x.xml")
if not system.path_exists(config_file):
with subprocess.Popen([self.get_executable(), "-help"]) as snes9x_process:
snes9x_process.communicate()
if not system.path_exists(config_file):
logger.error("Snes9x config file creation failed")
return
tree = etree.parse(config_file)
node = tree.find("./preferences/option[@name='%s']" % option)
if value.__class__.__name__ == "bool":
value = "1" if value else "0"
node.attrib["value"] = value
tree.write(config_file)
steam
¶
Steam for Linux runner
steam (Runner)
¶
Source code in lutris/runners/steam.py
class steam(Runner):
description = _("Runs Steam for Linux games")
human_name = _("Steam")
platforms = [_("Linux")]
runner_executable = "steam"
game_options = [
{
"option": "appid",
"label": _("Application ID"),
"type": "string",
"help": _(
"The application ID can be retrieved from the game's "
"page at steampowered.com. Example: 235320 is the "
"app ID for <i>Original War</i> in: \n"
"http://store.steampowered.com/app/<b>235320</b>/"
),
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _(
"Command line arguments used when launching the game.\n"
"Ignored when Steam Big Picture mode is enabled."
),
},
{
"option": "run_without_steam",
"label": _("DRM free mode (Do not launch Steam)"),
"type": "bool",
"default": False,
"advanced": True,
"help": _(
"Run the game directly without Steam, requires the game binary path to be set"
),
},
{
"option": "steamless_binary",
"type": "file",
"label": _("Game binary path"),
"advanced": True,
"help": _("Path to the game executable (Required by DRM free mode)"),
},
]
runner_options = [
{
"option": "quit_steam_on_exit",
"label": _("Stop Steam after game exits"),
"type": "bool",
"default": False,
"help": _(
"Shut down Steam after the game has quit\n"
"(only if Steam was started by Lutris)"
),
},
{
"option": "start_in_big_picture",
"label": _("Start Steam in Big Picture mode"),
"type": "bool",
"default": False,
"help": _(
"Launches Steam in Big Picture mode.\n"
"Only works if Steam is not running or "
"already running in Big Picture mode.\n"
"Useful when playing with a Steam Controller."
),
},
{
"option": "lsi_steam",
"label": _("Start Steam with LSI"),
"type": "bool",
"default": False,
"help": _(
"Launches steam with LSI patches enabled. "
"Make sure Lutris Runtime is disabled and "
"you have LSI installed. "
"https://github.com/solus-project/linux-steam-integration"
),
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"advanced": True,
"help": _("Extra command line arguments used when launching Steam"),
},
]
system_options_override = [{"option": "disable_runtime", "default": True}]
def __init__(self, config=None):
super().__init__(config)
self.own_game_remove_method = _("Remove game data (through Steam)")
self.no_game_remove_warning = True
self.original_steampid = None
@property
def runnable_alone(self):
return not linux.LINUX_SYSTEM.is_flatpak
@property
def appid(self):
return self.game_config.get("appid") or ""
def get_steam_config(self):
"""Return the "Steam" part of Steam's config.vdf as a dict."""
return read_config(self.steam_data_dir)
def get_library_config(self):
"""Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict """
return read_library_folders(self.steam_data_dir)
@property
def game_path(self):
if not self.appid:
return None
return self.get_game_path_from_appid(self.appid)
@property
def steam_data_dir(self):
"""Main installation directory for Steam"""
return get_steam_dir()
@property
def library_folders(self):
"""Return a list Steam library paths"""
return self.get_steamapps_dirs()
def get_appmanifest(self):
"""Return an AppManifest instance for the game"""
appmanifests = []
for apps_path in self.get_steamapps_dirs():
appmanifest = get_appmanifest_from_appid(apps_path, self.appid)
if appmanifest:
appmanifests.append(appmanifest)
if len(appmanifests) > 1:
logger.warning("More than one AppManifest for %s returning only 1st", self.appid)
if appmanifests:
return appmanifests[0]
def get_executable(self):
if linux.LINUX_SYSTEM.is_flatpak:
# Use xdg-open for Steam URIs in Flatpak
return system.find_executable("xdg-open")
if self.runner_config.get("lsi_steam") and system.find_executable("lsi-steam"):
return system.find_executable("lsi-steam")
runner_executable = self.runner_config.get("runner_executable")
if runner_executable and os.path.isfile(runner_executable):
return runner_executable
return system.find_executable(self.runner_executable)
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
if self.game_config.get("run_without_steam"):
steamless_binary = self.game_config.get("steamless_binary")
if steamless_binary and os.path.isfile(steamless_binary):
return os.path.dirname(steamless_binary)
return super().working_dir
@property
def launch_args(self):
"""Provide launch arguments for Steam"""
args = [self.get_executable()]
if linux.LINUX_SYSTEM.is_flatpak:
return args
if self.runner_config.get("start_in_big_picture"):
args.append("-bigpicture")
return args + split_arguments(self.runner_config.get("args") or "")
def get_game_path_from_appid(self, appid):
"""Return the game directory."""
for apps_path in self.get_steamapps_dirs():
game_path = get_path_from_appmanifest(apps_path, appid)
if game_path:
return game_path
logger.info("Data path for SteamApp %s not found.", appid)
def get_steamapps_dirs(self):
"""Return a list of the Steam library main + custom folders."""
dirs = []
# Extra colon-separated compatibility tools dirs environment variable
if 'STEAM_EXTRA_COMPAT_TOOLS_PATHS' in os.environ:
dirs += os.getenv('STEAM_EXTRA_COMPAT_TOOLS_PATHS').split(':')
# Main steamapps dir and compatibilitytools.d dir
for data_dir in STEAM_DATA_DIRS:
for _dir in ["steamapps", "compatibilitytools.d"]:
abs_dir = os.path.join(os.path.expanduser(data_dir), _dir)
abs_dir = system.fix_path_case(abs_dir)
if abs_dir and os.path.isdir(abs_dir):
dirs.append(abs_dir)
# Custom dirs
steam_config = self.get_steam_config()
if steam_config:
i = 1
while "BaseInstallFolder_%s" % i in steam_config:
path = steam_config["BaseInstallFolder_%s" % i] + "/steamapps"
path = system.fix_path_case(path)
if path and os.path.isdir(path):
dirs.append(path)
i += 1
# New Custom dirs
library_config = self.get_library_config()
if library_config:
paths = []
for entry in library_config.values():
if "mounted" in entry:
if entry.get("path") and entry.get("mounted") == "1":
path = system.fix_path_case(entry.get("path") + "/steamapps")
paths.append(path)
else:
path = system.fix_path_case(entry.get("path") + "/steamapps")
paths.append(path)
for path in paths:
if path and os.path.isdir(path):
dirs.append(path)
return system.list_unique_folders(dirs)
def get_default_steamapps_path(self):
steamapps_paths = self.get_steamapps_dirs()
if steamapps_paths:
return steamapps_paths[0]
def install(self, version=None, downloader=None, callback=None):
raise NonInstallableRunnerError(
"Steam for Linux installation is not handled by Lutris.\n"
"Please go to "
"<a href='http://steampowered.com'>http://steampowered.com</a>"
" or install Steam with the package provided by your distribution."
)
def install_game(self, appid, generate_acf=False):
logger.debug("Installing steam game %s", appid)
if generate_acf:
acf_data = get_default_acf(appid, appid)
acf_content = to_vdf(acf_data)
steamapps_path = self.get_default_steamapps_path()
if not steamapps_path:
raise RuntimeError("Could not find Steam path, is Steam installed?")
acf_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
with open(acf_path, "w", encoding='utf-8') as acf_file:
acf_file.write(acf_content)
if is_running():
shutdown()
time.sleep(5)
command = [self.get_executable(), "steam://install/%s" % appid]
subprocess.Popen(command) # pylint: disable=consider-using-with
def prelaunch(self):
def has_steam_shutdown(times=10):
for __ in range(times):
time.sleep(1)
if not is_running():
return True
# If using primusrun, shutdown existing Steam first
if self.system_config.get("optimus") != "off" and is_running():
shutdown()
if not has_steam_shutdown():
logger.info("Forcing Steam shutdown")
kill()
if not has_steam_shutdown(5):
logger.error("Failed to shut down Steam :(")
return False
return True
def get_run_data(self):
return {"command": self.launch_args, "env": self.get_env()}
def play(self):
game_args = self.game_config.get("args") or ""
binary_path = self.game_config.get("steamless_binary")
if self.game_config.get("run_without_steam") and binary_path:
# Start without steam
if not system.path_exists(binary_path):
return {"error": "FILE_NOT_FOUND", "file": binary_path}
self.original_steampid = None
command = [binary_path]
else:
# Start through steam
if linux.LINUX_SYSTEM.is_flatpak:
if game_args:
steam_uri = "steam://run/%s//%s/" % (self.appid, game_args)
else:
steam_uri = "steam://rungameid/%s" % self.appid
return {
"command": self.launch_args + [steam_uri],
"env": self.get_env(),
}
# Get current steam pid to act as the root pid instead of lutris
self.original_steampid = get_steam_pid()
command = self.launch_args
if self.runner_config.get("start_in_big_picture") or not game_args:
command.append("steam://rungameid/%s" % self.appid)
else:
command.append("-applaunch")
command.append(self.appid)
if game_args:
for arg in split_arguments(game_args):
command.append(arg)
return {
"command": command,
"env": self.get_env(),
}
def stop(self):
if self.runner_config.get("quit_steam_on_exit") and not self.original_steampid:
shutdown()
return True
return False
def remove_game_data(self, appid=None, **kwargs):
if not self.is_installed():
return False
command = MonitoredCommand(
[self.get_executable(), "steam://uninstall/%s" % (appid or self.appid)],
runner=self,
env=self.get_env(),
)
command.start()
appid
property
readonly
¶
description
¶
game_options
¶
game_path
property
readonly
¶
Return the directory where the game is installed.
human_name
¶
launch_args
property
readonly
¶
Provide launch arguments for Steam
library_folders
property
readonly
¶
Return a list Steam library paths
platforms
¶
runnable_alone
property
readonly
¶
bool(x) -> bool
Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
runner_executable
¶
runner_options
¶
steam_data_dir
property
readonly
¶
Main installation directory for Steam
system_options_override
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
__init__(self, config=None)
special
¶
Source code in lutris/runners/steam.py
def __init__(self, config=None):
super().__init__(config)
self.own_game_remove_method = _("Remove game data (through Steam)")
self.no_game_remove_warning = True
self.original_steampid = None
get_appmanifest(self)
¶
Return an AppManifest instance for the game
Source code in lutris/runners/steam.py
def get_appmanifest(self):
"""Return an AppManifest instance for the game"""
appmanifests = []
for apps_path in self.get_steamapps_dirs():
appmanifest = get_appmanifest_from_appid(apps_path, self.appid)
if appmanifest:
appmanifests.append(appmanifest)
if len(appmanifests) > 1:
logger.warning("More than one AppManifest for %s returning only 1st", self.appid)
if appmanifests:
return appmanifests[0]
get_default_steamapps_path(self)
¶
Source code in lutris/runners/steam.py
def get_default_steamapps_path(self):
steamapps_paths = self.get_steamapps_dirs()
if steamapps_paths:
return steamapps_paths[0]
get_executable(self)
¶
Source code in lutris/runners/steam.py
def get_executable(self):
if linux.LINUX_SYSTEM.is_flatpak:
# Use xdg-open for Steam URIs in Flatpak
return system.find_executable("xdg-open")
if self.runner_config.get("lsi_steam") and system.find_executable("lsi-steam"):
return system.find_executable("lsi-steam")
runner_executable = self.runner_config.get("runner_executable")
if runner_executable and os.path.isfile(runner_executable):
return runner_executable
return system.find_executable(self.runner_executable)
get_game_path_from_appid(self, appid)
¶
Return the game directory.
Source code in lutris/runners/steam.py
def get_game_path_from_appid(self, appid):
"""Return the game directory."""
for apps_path in self.get_steamapps_dirs():
game_path = get_path_from_appmanifest(apps_path, appid)
if game_path:
return game_path
logger.info("Data path for SteamApp %s not found.", appid)
get_library_config(self)
¶
Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict
Source code in lutris/runners/steam.py
def get_library_config(self):
"""Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict """
return read_library_folders(self.steam_data_dir)
get_run_data(self)
¶
Return dict with command (exe & args list) and env vars (dict).
Reimplement in derived runner if need be.
Source code in lutris/runners/steam.py
def get_run_data(self):
return {"command": self.launch_args, "env": self.get_env()}
get_steam_config(self)
¶
Return the "Steam" part of Steam's config.vdf as a dict.
Source code in lutris/runners/steam.py
def get_steam_config(self):
"""Return the "Steam" part of Steam's config.vdf as a dict."""
return read_config(self.steam_data_dir)
get_steamapps_dirs(self)
¶
Return a list of the Steam library main + custom folders.
Source code in lutris/runners/steam.py
def get_steamapps_dirs(self):
"""Return a list of the Steam library main + custom folders."""
dirs = []
# Extra colon-separated compatibility tools dirs environment variable
if 'STEAM_EXTRA_COMPAT_TOOLS_PATHS' in os.environ:
dirs += os.getenv('STEAM_EXTRA_COMPAT_TOOLS_PATHS').split(':')
# Main steamapps dir and compatibilitytools.d dir
for data_dir in STEAM_DATA_DIRS:
for _dir in ["steamapps", "compatibilitytools.d"]:
abs_dir = os.path.join(os.path.expanduser(data_dir), _dir)
abs_dir = system.fix_path_case(abs_dir)
if abs_dir and os.path.isdir(abs_dir):
dirs.append(abs_dir)
# Custom dirs
steam_config = self.get_steam_config()
if steam_config:
i = 1
while "BaseInstallFolder_%s" % i in steam_config:
path = steam_config["BaseInstallFolder_%s" % i] + "/steamapps"
path = system.fix_path_case(path)
if path and os.path.isdir(path):
dirs.append(path)
i += 1
# New Custom dirs
library_config = self.get_library_config()
if library_config:
paths = []
for entry in library_config.values():
if "mounted" in entry:
if entry.get("path") and entry.get("mounted") == "1":
path = system.fix_path_case(entry.get("path") + "/steamapps")
paths.append(path)
else:
path = system.fix_path_case(entry.get("path") + "/steamapps")
paths.append(path)
for path in paths:
if path and os.path.isdir(path):
dirs.append(path)
return system.list_unique_folders(dirs)
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/steam.py
def install(self, version=None, downloader=None, callback=None):
raise NonInstallableRunnerError(
"Steam for Linux installation is not handled by Lutris.\n"
"Please go to "
"<a href='http://steampowered.com'>http://steampowered.com</a>"
" or install Steam with the package provided by your distribution."
)
install_game(self, appid, generate_acf=False)
¶
Source code in lutris/runners/steam.py
def install_game(self, appid, generate_acf=False):
logger.debug("Installing steam game %s", appid)
if generate_acf:
acf_data = get_default_acf(appid, appid)
acf_content = to_vdf(acf_data)
steamapps_path = self.get_default_steamapps_path()
if not steamapps_path:
raise RuntimeError("Could not find Steam path, is Steam installed?")
acf_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
with open(acf_path, "w", encoding='utf-8') as acf_file:
acf_file.write(acf_content)
if is_running():
shutdown()
time.sleep(5)
command = [self.get_executable(), "steam://install/%s" % appid]
subprocess.Popen(command) # pylint: disable=consider-using-with
play(self)
¶
Source code in lutris/runners/steam.py
def play(self):
game_args = self.game_config.get("args") or ""
binary_path = self.game_config.get("steamless_binary")
if self.game_config.get("run_without_steam") and binary_path:
# Start without steam
if not system.path_exists(binary_path):
return {"error": "FILE_NOT_FOUND", "file": binary_path}
self.original_steampid = None
command = [binary_path]
else:
# Start through steam
if linux.LINUX_SYSTEM.is_flatpak:
if game_args:
steam_uri = "steam://run/%s//%s/" % (self.appid, game_args)
else:
steam_uri = "steam://rungameid/%s" % self.appid
return {
"command": self.launch_args + [steam_uri],
"env": self.get_env(),
}
# Get current steam pid to act as the root pid instead of lutris
self.original_steampid = get_steam_pid()
command = self.launch_args
if self.runner_config.get("start_in_big_picture") or not game_args:
command.append("steam://rungameid/%s" % self.appid)
else:
command.append("-applaunch")
command.append(self.appid)
if game_args:
for arg in split_arguments(game_args):
command.append(arg)
return {
"command": command,
"env": self.get_env(),
}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/steam.py
def prelaunch(self):
def has_steam_shutdown(times=10):
for __ in range(times):
time.sleep(1)
if not is_running():
return True
# If using primusrun, shutdown existing Steam first
if self.system_config.get("optimus") != "off" and is_running():
shutdown()
if not has_steam_shutdown():
logger.info("Forcing Steam shutdown")
kill()
if not has_steam_shutdown(5):
logger.error("Failed to shut down Steam :(")
return False
return True
remove_game_data(self, appid=None, **kwargs)
¶
Source code in lutris/runners/steam.py
def remove_game_data(self, appid=None, **kwargs):
if not self.is_installed():
return False
command = MonitoredCommand(
[self.get_executable(), "steam://uninstall/%s" % (appid or self.appid)],
runner=self,
env=self.get_env(),
)
command.start()
stop(self)
¶
Source code in lutris/runners/steam.py
def stop(self):
if self.runner_config.get("quit_steam_on_exit") and not self.original_steampid:
shutdown()
return True
return False
get_steam_pid()
¶
Return pid of Steam process.
Source code in lutris/runners/steam.py
def get_steam_pid():
"""Return pid of Steam process."""
return system.get_pid("steam$")
is_running()
¶
Checks if Steam is running.
Source code in lutris/runners/steam.py
def is_running():
"""Checks if Steam is running."""
return bool(get_steam_pid())
kill()
¶
Force quit Steam.
Source code in lutris/runners/steam.py
def kill():
"""Force quit Steam."""
system.kill_pid(get_steam_pid())
shutdown()
¶
Cleanly quit Steam.
Source code in lutris/runners/steam.py
def shutdown():
"""Cleanly quit Steam."""
logger.debug("Shutting down Steam")
if is_running():
subprocess.call(["steam", "-shutdown"])
vice
¶
vice (Runner)
¶
Source code in lutris/runners/vice.py
class vice(Runner):
description = _("Commodore Emulator")
human_name = _("Vice")
platforms = [
_("Commodore 64"),
_("Commodore 128"),
_("Commodore VIC20"),
_("Commodore PET"),
_("Commodore Plus/4"),
_("Commodore CBM II"),
]
machine_choices = [
("C64", "c64"),
("C128", "c128"),
("vic20", "vic20"),
("PET", "pet"),
("Plus/4", "plus4"),
("CBM-II", "cbmii"),
]
game_options = [
{
"option":
"main_file",
"type":
"file",
"label":
_("ROM file"),
"help": _(
"The game data, commonly called a ROM image.\n"
"Supported formats: X64, D64, G64, P64, D67, D71, D81, "
"D80, D82, D1M, D2M, D4M, T46, P00 and CRT."
),
}
]
runner_options = [
{
"option": "joy",
"type": "bool",
"label": _("Use joysticks"),
"default": False
},
{
"option": "fullscreen",
"type": "bool",
"label": _("Fullscreen"),
"default": False,
},
{
"option": "double",
"type": "bool",
"label": _("Scale up display by 2"),
"default": True,
},
{
"option": "aspect_ratio",
"type": "bool",
"label": _("Preserve aspect ratio"),
"default": True,
},
{
"option": "drivesound",
"type": "bool",
"label": _("Enable sound emulation of disk drives"),
"default": False,
},
{
"option": "renderer",
"type": "choice",
"label": _("Graphics renderer"),
"choices": [("OpenGL", "opengl"), (_("Software"), "software")],
"default": "opengl",
},
{
"option": "machine",
"type": "choice",
"label": _("Machine"),
"choices": machine_choices,
"default": "c64",
},
]
def get_platform(self):
machine = self.game_config.get("machine")
if machine:
for index, choice in enumerate(self.machine_choices):
if choice[1] == machine:
return self.platforms[index]
return self.platforms[0] # Default to C64
def get_executable(self, machine=None):
if not machine:
machine = "c64"
executables = {
"c64": "x64",
"c128": "x128",
"vic20": "xvic",
"pet": "xpet",
"plus4": "xplus4",
"cbmii": "xcbm2",
}
try:
executable = executables[machine]
except KeyError as ex:
raise ValueError("Invalid machine '%s'" % machine) from ex
return os.path.join(settings.RUNNER_DIR, "vice/bin/%s" % executable)
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
config_path = system.create_folder("~/.vice")
lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib/vice")
if not system.path_exists(lib_dir):
lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib64/vice")
if not system.path_exists(lib_dir):
logger.error("Missing lib folder in the Vice runner")
else:
system.merge_folders(lib_dir, config_path)
if callback:
callback()
super().install(version, downloader, on_runner_installed)
def get_roms_path(self, machine=None):
if not machine:
machine = "c64"
paths = {
"c64": "C64",
"c128": "C128",
"vic20": "VIC20",
"pet": "PET",
"plus4": "PLUS4",
"cmbii": "CBM-II",
}
root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
return os.path.join(root_dir, "lib64/vice", paths[machine])
@staticmethod
def get_option_prefix(machine):
prefixes = {
"c64": "VICII",
"c128": "VICII",
"vic20": "VIC",
"pet": "CRTC",
"plus4": "TED",
"cmbii": "CRTC",
}
return prefixes[machine]
@staticmethod
def get_joydevs(machine):
joydevs = {"c64": 2, "c128": 2, "vic20": 1, "pet": 0, "plus4": 2, "cmbii": 0}
return joydevs[machine]
@staticmethod
def get_rom_args(machine, rom):
args = []
if rom.endswith(".crt"):
crt_option = {
"c64": "-cartcrt",
"c128": "-cartcrt",
"vic20": "-cartgeneric",
"pet": None,
"plus4": "-cart",
"cmbii": None,
}
if crt_option[machine]:
args.append(crt_option[machine])
args.append(rom)
return args
def play(self):
machine = self.runner_config.get("machine")
rom = self.game_config.get("main_file")
if not rom:
return {"error": "CUSTOM", "text": "No rom provided"}
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
params = [self.get_executable(machine)]
rom_dir = os.path.dirname(rom)
params.append("-chdir")
params.append(rom_dir)
option_prefix = self.get_option_prefix(machine)
if self.runner_config.get("fullscreen"):
params.append("-{}full".format(option_prefix))
if self.runner_config.get("double"):
params.append("-{}dsize".format(option_prefix))
if self.runner_config.get("renderer"):
params.append("-sdl2renderer")
params.append(self.runner_config["renderer"])
if not self.runner_config.get("aspect_ratio", True):
params.append("-sdlaspectmode")
params.append("0")
if self.runner_config.get("drivesound"):
params.append("-drivesound")
if self.runner_config.get("joy"):
for dev in range(self.get_joydevs(machine)):
params += ["-joydev{}".format(dev + 1), "4"]
params.extend(self.get_rom_args(machine, rom))
return {"command": params}
description
¶
game_options
¶
human_name
¶
machine_choices
¶
platforms
¶
runner_options
¶
get_executable(self, machine=None)
¶
Source code in lutris/runners/vice.py
def get_executable(self, machine=None):
if not machine:
machine = "c64"
executables = {
"c64": "x64",
"c128": "x128",
"vic20": "xvic",
"pet": "xpet",
"plus4": "xplus4",
"cbmii": "xcbm2",
}
try:
executable = executables[machine]
except KeyError as ex:
raise ValueError("Invalid machine '%s'" % machine) from ex
return os.path.join(settings.RUNNER_DIR, "vice/bin/%s" % executable)
get_joydevs(machine)
staticmethod
¶
Source code in lutris/runners/vice.py
@staticmethod
def get_joydevs(machine):
joydevs = {"c64": 2, "c128": 2, "vic20": 1, "pet": 0, "plus4": 2, "cmbii": 0}
return joydevs[machine]
get_option_prefix(machine)
staticmethod
¶
Source code in lutris/runners/vice.py
@staticmethod
def get_option_prefix(machine):
prefixes = {
"c64": "VICII",
"c128": "VICII",
"vic20": "VIC",
"pet": "CRTC",
"plus4": "TED",
"cmbii": "CRTC",
}
return prefixes[machine]
get_platform(self)
¶
Source code in lutris/runners/vice.py
def get_platform(self):
machine = self.game_config.get("machine")
if machine:
for index, choice in enumerate(self.machine_choices):
if choice[1] == machine:
return self.platforms[index]
return self.platforms[0] # Default to C64
get_rom_args(machine, rom)
staticmethod
¶
Source code in lutris/runners/vice.py
@staticmethod
def get_rom_args(machine, rom):
args = []
if rom.endswith(".crt"):
crt_option = {
"c64": "-cartcrt",
"c128": "-cartcrt",
"vic20": "-cartgeneric",
"pet": None,
"plus4": "-cart",
"cmbii": None,
}
if crt_option[machine]:
args.append(crt_option[machine])
args.append(rom)
return args
get_roms_path(self, machine=None)
¶
Source code in lutris/runners/vice.py
def get_roms_path(self, machine=None):
if not machine:
machine = "c64"
paths = {
"c64": "C64",
"c128": "C128",
"vic20": "VIC20",
"pet": "PET",
"plus4": "PLUS4",
"cmbii": "CBM-II",
}
root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
return os.path.join(root_dir, "lib64/vice", paths[machine])
install(self, version=None, downloader=None, callback=None)
¶
Install runner using package management systems.
Source code in lutris/runners/vice.py
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
config_path = system.create_folder("~/.vice")
lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib/vice")
if not system.path_exists(lib_dir):
lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib64/vice")
if not system.path_exists(lib_dir):
logger.error("Missing lib folder in the Vice runner")
else:
system.merge_folders(lib_dir, config_path)
if callback:
callback()
super().install(version, downloader, on_runner_installed)
play(self)
¶
Source code in lutris/runners/vice.py
def play(self):
machine = self.runner_config.get("machine")
rom = self.game_config.get("main_file")
if not rom:
return {"error": "CUSTOM", "text": "No rom provided"}
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
params = [self.get_executable(machine)]
rom_dir = os.path.dirname(rom)
params.append("-chdir")
params.append(rom_dir)
option_prefix = self.get_option_prefix(machine)
if self.runner_config.get("fullscreen"):
params.append("-{}full".format(option_prefix))
if self.runner_config.get("double"):
params.append("-{}dsize".format(option_prefix))
if self.runner_config.get("renderer"):
params.append("-sdl2renderer")
params.append(self.runner_config["renderer"])
if not self.runner_config.get("aspect_ratio", True):
params.append("-sdlaspectmode")
params.append("0")
if self.runner_config.get("drivesound"):
params.append("-drivesound")
if self.runner_config.get("joy"):
for dev in range(self.get_joydevs(machine)):
params += ["-joydev{}".format(dev + 1), "4"]
params.extend(self.get_rom_args(machine, rom))
return {"command": params}
web
¶
Run web based games
DEFAULT_ICON
¶
web (Runner)
¶
Source code in lutris/runners/web.py
class web(Runner):
human_name = _("Web")
description = _("Runs web based games")
platforms = [_("Web")]
game_options = [
{
"option": "main_file",
"type": "string",
"label": _("Full URL or HTML file path"),
"help": _("The full address of the game's web page or path to a HTML file."),
}
]
runner_options = [
{
"option": "fullscreen",
"label": _("Open in fullscreen"),
"type": "bool",
"default": False,
"help": _("Launch the game in fullscreen."),
},
{
"option": "maximize_window",
"label": _("Open window maximized"),
"type": "bool",
"default": False,
"help": _("Maximizes the window when game starts."),
},
{
"option": "window_size",
"label": _("Window size"),
"type": "choice_with_entry",
"choices": [
"640x480",
"800x600",
"1024x768",
"1280x720",
"1280x1024",
"1920x1080",
],
"default": "800x600",
"help": _("The initial size of the game window when not opened."),
},
{
"option": "disable_resizing",
"label": _("Disable window resizing (disables fullscreen and maximize)"),
"type": "bool",
"default": False,
"help": _("You can't resize this window."),
},
{
"option": "frameless",
"label": _("Borderless window"),
"type": "bool",
"default": False,
"help": _("The window has no borders/frame."),
},
{
"option": "disable_menu_bar",
"label": _("Disable menu bar and default shortcuts"),
"type": "bool",
"default": False,
"help": _("This also disables default keyboard shortcuts, "
"like copy/paste and fullscreen toggling."),
},
{
"option": "disable_scrolling",
"label": _("Disable page scrolling and hide scrollbars"),
"type": "bool",
"default": False,
"help": _("Disables scrolling on the page."),
},
{
"option": "hide_cursor",
"label": _("Hide mouse cursor"),
"type": "bool",
"default": False,
"help": _("Prevents the mouse cursor from showing "
"when hovering above the window."),
},
{
"option":
"open_links",
"label":
_("Open links in game window"),
"type":
"bool",
"default":
False,
"help": _(
"Enable this option if you want clicked links to open inside the "
"game window. By default all links open in your default web browser."
),
},
{
"option": "remove_margin",
"label": _("Remove default <body> margin & padding"),
"type": "bool",
"default": False,
"help": _("Sets margin and padding to zero "
"on <html> and <body> elements."),
},
{
"option": "enable_flash",
"label": _("Enable Adobe Flash Player"),
"type": "bool",
"default": False,
"help": _("Enable Adobe Flash Player."),
},
{
"option": "user_agent",
"label": _("Custom User-Agent"),
"type": "string",
"default": "",
"help": _("Overrides the default User-Agent header used by the runner."),
"advanced": True,
},
{
"option": "devtools",
"label": _("Debug with Developer Tools"),
"type": "bool",
"default": False,
"help": _("Let's you debug the page."),
"advanced": True,
},
{
"option": "external_browser",
"label": _("Open in web browser (old behavior)"),
"type": "bool",
"default": False,
"help": _("Launch the game in a web browser."),
},
{
"option":
"custom_browser_executable",
"label":
_("Custom web browser executable"),
"type":
"file",
"help": _(
"Select the executable of a browser on your system.\n"
"If left blank, Lutris will launch your default browser (xdg-open)."
),
},
{
"option":
"custom_browser_args",
"label":
_("Web browser arguments"),
"type":
"string",
"default":
'"$GAME"',
"help": _(
"Command line arguments to pass to the executable.\n"
"$GAME or $URL inserts the game url.\n\n"
'For Chrome/Chromium app mode use: --app="$GAME"'
),
},
]
system_options_override = [{"option": "disable_runtime", "default": True}]
runner_executable = "web/electron/electron"
def get_env(self, os_env=True):
env = super().get_env(os_env)
enable_flash_player = self.runner_config.get("enable_flash")
env["ENABLE_FLASH_PLAYER"] = "1" if enable_flash_player else "0"
return env
def play(self):
url = self.game_config.get("main_file")
if not url:
return {
"error": "CUSTOM",
"text": _("The web address is empty, \n"
"verify the game's configuration."),
}
# check if it's an url or a file
is_url = urlparse(url).scheme != ""
if not is_url:
if not system.path_exists(url):
return {
"error": "CUSTOM",
"text": _("The file %s does not exist, \n"
"verify the game's configuration.") % url,
}
url = "file://" + url
game_data = get_game_by_field(self.config.game_config_id, "configpath")
# keep the old behavior from browser runner, but with support for extra arguments!
if self.runner_config.get("external_browser"):
# is it possible to disable lutris runtime here?
browser = self.runner_config.get("custom_browser_executable") or "xdg-open"
args = self.runner_config.get("custom_browser_args")
args = args or '"$GAME"'
arguments = string.Template(args).safe_substitute({"GAME": url, "URL": url})
command = [browser]
for arg in split_arguments(arguments):
command.append(arg)
return {"command": command}
icon = resources.get_icon_path(game_data.get("slug"))
if not system.path_exists(icon):
icon = DEFAULT_ICON
command = [
self.get_executable(),
os.path.join(settings.RUNNER_DIR, "web/electron/resources/app.asar"),
url,
"--name",
game_data.get("name"),
"--icon",
icon,
]
for key in [
"fullscreen",
"frameless",
"devtools",
"disable_resizing",
"disable_menu_bar",
"maximize_window",
"disable_scrolling",
"hide_cursor",
"open_links",
"remove_margin",
]:
if self.runner_config.get(key):
converted_opt_name = key.replace("_", "-")
command.append("--{option}".format(option=converted_opt_name))
if self.runner_config.get("window_size"):
command.append("--window-size")
command.append(self.runner_config.get("window_size"))
if self.runner_config.get("user_agent"):
command.append("--user-agent")
command.append(self.runner_config.get("user_agent"))
if linux.LINUX_SYSTEM.is_flatpak:
command.append("--no-sandbox")
return {"command": command, "env": self.get_env(False)}
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
system_options_override
¶
get_env(self, os_env=True)
¶
Return environment variables used for a game.
Source code in lutris/runners/web.py
def get_env(self, os_env=True):
env = super().get_env(os_env)
enable_flash_player = self.runner_config.get("enable_flash")
env["ENABLE_FLASH_PLAYER"] = "1" if enable_flash_player else "0"
return env
play(self)
¶
Source code in lutris/runners/web.py
def play(self):
url = self.game_config.get("main_file")
if not url:
return {
"error": "CUSTOM",
"text": _("The web address is empty, \n"
"verify the game's configuration."),
}
# check if it's an url or a file
is_url = urlparse(url).scheme != ""
if not is_url:
if not system.path_exists(url):
return {
"error": "CUSTOM",
"text": _("The file %s does not exist, \n"
"verify the game's configuration.") % url,
}
url = "file://" + url
game_data = get_game_by_field(self.config.game_config_id, "configpath")
# keep the old behavior from browser runner, but with support for extra arguments!
if self.runner_config.get("external_browser"):
# is it possible to disable lutris runtime here?
browser = self.runner_config.get("custom_browser_executable") or "xdg-open"
args = self.runner_config.get("custom_browser_args")
args = args or '"$GAME"'
arguments = string.Template(args).safe_substitute({"GAME": url, "URL": url})
command = [browser]
for arg in split_arguments(arguments):
command.append(arg)
return {"command": command}
icon = resources.get_icon_path(game_data.get("slug"))
if not system.path_exists(icon):
icon = DEFAULT_ICON
command = [
self.get_executable(),
os.path.join(settings.RUNNER_DIR, "web/electron/resources/app.asar"),
url,
"--name",
game_data.get("name"),
"--icon",
icon,
]
for key in [
"fullscreen",
"frameless",
"devtools",
"disable_resizing",
"disable_menu_bar",
"maximize_window",
"disable_scrolling",
"hide_cursor",
"open_links",
"remove_margin",
]:
if self.runner_config.get(key):
converted_opt_name = key.replace("_", "-")
command.append("--{option}".format(option=converted_opt_name))
if self.runner_config.get("window_size"):
command.append("--window-size")
command.append(self.runner_config.get("window_size"))
if self.runner_config.get("user_agent"):
command.append("--user-agent")
command.append(self.runner_config.get("user_agent"))
if linux.LINUX_SYSTEM.is_flatpak:
command.append("--no-sandbox")
return {"command": command, "env": self.get_env(False)}
wine
¶
Wine runner
DEFAULT_WINE_PREFIX
¶
MIN_SAFE_VERSION
¶
wine (Runner)
¶
Source code in lutris/runners/wine.py
class wine(Runner):
description = _("Runs Windows games")
human_name = _("Wine")
platforms = [_("Windows")]
multiple_versions = True
entry_point_option = "exe"
game_options = [
{
"option": "exe",
"type": "file",
"label": _("Executable"),
"help": _("The game's main EXE file"),
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Windows command line arguments used when launching the game"),
"validator": shlex.split
},
{
"option": "working_dir",
"type": "directory_chooser",
"label": _("Working directory"),
"help": _(
"The location where the game is run from.\n"
"By default, Lutris uses the directory of the "
"executable."
),
},
{
"option": "prefix",
"type": "directory_chooser",
"label": _("Wine prefix"),
"help": _(
'The prefix used by Wine.\n'
"It's a directory containing a set of files and "
"folders making up a confined Windows environment."
),
},
{
"option": "arch",
"type": "choice",
"label": _("Prefix architecture"),
"choices": [(_("Auto"), "auto"), (_("32-bit"), "win32"), (_("64-bit"), "win64")],
"default": "auto",
"help": _("The architecture of the Windows environment"),
},
]
reg_prefix = "HKEY_CURRENT_USER/Software/Wine"
reg_keys = {
"Audio": r"%s/Drivers" % reg_prefix,
"MouseWarpOverride": r"%s/DirectInput" % reg_prefix,
"Desktop": "MANAGED",
"WineDesktop": "MANAGED",
"ShowCrashDialog": "MANAGED"
}
core_processes = (
"services.exe",
"winedevice.exe",
"plugplay.exe",
"explorer.exe",
"rpcss.exe",
"rundll32.exe",
"wineboot.exe",
)
def __init__(self, config=None): # noqa: C901
super().__init__(config)
self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy() # we'll modify this, so we better copy it
def get_wine_version_choices():
version_choices = [(_("Custom (select executable below)"), "custom")]
labels = {
"winehq-devel": _("WineHQ Devel ({})"),
"winehq-staging": _("WineHQ Staging ({})"),
"wine-development": _("Wine Development ({})"),
"system": _("System ({})"),
}
versions = get_wine_versions()
for version in versions:
if version in labels:
version_number = get_wine_version(WINE_PATHS[version])
label = labels[version].format(version_number)
else:
label = version
version_choices.append((label, version))
return version_choices
def esync_limit_callback(widget, option, config):
limits_set = is_esync_limit_set()
wine_path = self.get_path_for_version(config["version"])
wine_ver = is_version_esync(wine_path)
response = True
if not wine_ver:
response = thread_safe_call(esync_display_version_warning)
if not limits_set:
thread_safe_call(esync_display_limit_warning)
response = False
return widget, option, response
def fsync_support_callback(widget, option, config):
fsync_supported = is_fsync_supported()
wine_path = self.get_path_for_version(config["version"])
wine_ver = is_version_fsync(wine_path)
response = True
if not wine_ver:
response = thread_safe_call(fsync_display_version_warning)
if not fsync_supported:
thread_safe_call(fsync_display_support_warning)
response = False
return widget, option, response
def dxvk_vulkan_callback(widget, option, config):
response = True
if not is_vulkan_supported():
if not thread_safe_call(display_vulkan_error):
response = False
return widget, option, response
self.runner_options = [
{
"option": "version",
"label": _("Wine version"),
"type": "choice",
"choices": get_wine_version_choices,
"default": get_default_version(),
"help": _(
"The version of Wine used to launch the game.\n"
"Using the last version is generally recommended, "
"but some games work better on older versions."
),
},
{
"option": "custom_wine_path",
"label": _("Custom Wine executable"),
"type": "file",
"advanced": True,
"help": _("The Wine executable to be used if you have "
'selected "Custom" as the Wine version.'),
},
{
"option": "system_winetricks",
"label": _("Use system winetricks"),
"type": "bool",
"default": False,
"advanced": True,
"help": _("Switch on to use /usr/bin/winetricks for winetricks."),
},
{
"option": "dxvk",
"label": _("Enable DXVK"),
"type": "extended_bool",
"callback": dxvk_vulkan_callback,
"callback_on": True,
"default": True,
"active": True,
"help": _(
"Use DXVK to "
"increase compatibility and performance in Direct3D 11, 10 "
"and 9 applications by translating their calls to Vulkan."),
},
{
"option": "dxvk_version",
"label": _("DXVK version"),
"advanced": True,
"type": "choice_with_entry",
"choices": DXVKManager().version_choices,
"default": DXVKManager().version,
},
{
"option": "vkd3d",
"label": _("Enable VKD3D"),
"type": "extended_bool",
"callback": dxvk_vulkan_callback,
"callback_on": True,
"default": True,
"active": True,
"help": _(
"Use VKD3D to enable support for Direct3D 12 "
"applications by translating their calls to Vulkan."),
},
{
"option": "vkd3d_version",
"label": _("VKD3D version"),
"advanced": True,
"type": "choice_with_entry",
"choices": VKD3DManager().version_choices,
"default": VKD3DManager().version,
},
{
"option": "d3d_extras",
"label": _("Enable D3D Extras"),
"type": "bool",
"default": True,
"advanced": True,
"help": _(
"Replace Wine's D3DX and D3DCOMPILER libraries with alternative ones. "
"Needed for proper functionality of DXVK with some games."
),
},
{
"option": "d3d_extras_version",
"label": _("D3D Extras version"),
"advanced": True,
"type": "choice_with_entry",
"choices": D3DExtrasManager().version_choices,
"default": D3DExtrasManager().version,
},
{
"option": "dxvk_nvapi",
"label": _("Enable DXVK-NVAPI / DLSS"),
"type": "bool",
"default": True,
"advanced": True,
"help": _(
"Enable emulation of Nvidia's NVAPI and add DLSS support, if available."
),
},
{
"option": "dxvk_nvapi_version",
"label": _("DXVK NVAPI version"),
"advanced": True,
"type": "choice_with_entry",
"choices": DXVKNVAPIManager().version_choices,
"default": DXVKNVAPIManager().version,
},
{
"option": "dgvoodoo2",
"label": _("Enable dgvoodoo2"),
"type": "bool",
"default": False,
"advanced": False,
"help": _(
"dgvoodoo2 is an alternative translation layer for rendering old games "
"that utilize D3D1-7 and Glide APIs. As it translates to D3D11, it's "
"recommended to use it in combination with DXVK. Only 32-bit apps are supported."
),
},
{
"option": "dgvoodoo2_version",
"label": _("dgvoodoo2 version"),
"advanced": True,
"type": "choice_with_entry",
"choices": dgvoodoo2Manager().version_choices,
"default": dgvoodoo2Manager().version,
},
{
"option": "esync",
"label": _("Enable Esync"),
"type": "extended_bool",
"callback": esync_limit_callback,
"callback_on": True,
"active": True,
"default": True,
"help": _(
"Enable eventfd-based synchronization (esync). "
"This will increase performance in applications "
"that take advantage of multi-core processors."
),
},
{
"option": "fsync",
"label": _("Enable Fsync"),
"type": "extended_bool",
"default": True,
"callback": fsync_support_callback,
"callback_on": True,
"active": True,
"help": _(
"Enable futex-based synchronization (fsync). "
"This will increase performance in applications "
"that take advantage of multi-core processors. "
"Requires a custom kernel with the fsync patchset."
),
},
{
"option": "fsr",
"label": _("Enable AMD FidelityFX Super Resolution (FSR)"),
"type": "bool",
"default": True,
"help": _(
"Use FSR to upscale the game window to native resolution.\n"
"Requires Lutris Wine FShack >= 6.13 and setting the game to a lower resolution.\n"
"Does not work with games running in borderless window mode or that perform their own upscaling."
),
},
{
"option": "battleye",
"label": _("Enable BattlEye Anti-Cheat"),
"type": "bool",
"default": False,
"help": _(
"Enable support for BattlEye Anti-Cheat in supported games\n"
"Requires Lutris Wine 6.21-2 and newer or any other compatible Wine build.\n"
),
},
{
"option": "Desktop",
"label": _("Windowed (virtual desktop)"),
"type": "bool",
"default": False,
"help": _(
"Run the whole Windows desktop in a window.\n"
"Otherwise, run it fullscreen.\n"
"This corresponds to Wine's Virtual Desktop option."
),
},
{
"option": "WineDesktop",
"label": _("Virtual desktop resolution"),
"type": "choice_with_entry",
"choices": DISPLAY_MANAGER.get_resolutions,
"help": _("The size of the virtual desktop in pixels."),
},
{
"option": "Dpi",
"label": _("Enable DPI Scaling"),
"type": "bool",
"default": False,
"help": _(
"Enables the Windows application's DPI scaling.\n"
"Otherwise, disables DPI scaling by using 96 DPI.\n"
"This corresponds to Wine's Screen Resolution option."
),
},
{
"option": "ExplicitDpi",
"label": _("DPI"),
"type": "string",
"help": _(
"The DPI to be used if 'Enable DPI Scaling' is turned on.\n"
"If blank or 'auto', Lutris will auto-detect this."
),
},
{
"option": "MouseWarpOverride",
"label": _("Mouse Warp Override"),
"type": "choice",
"choices": [
(_("Enable"), "enable"),
(_("Disable"), "disable"),
(_("Force"), "force"),
],
"default": "enable",
"advanced": True,
"help": _(
"Override the default mouse pointer warping behavior\n"
"<b>Enable</b>: (Wine default) warp the pointer when the "
"mouse is exclusively acquired \n"
"<b>Disable</b>: never warp the mouse pointer \n"
"<b>Force</b>: always warp the pointer"
),
},
{
"option": "Audio",
"label": _("Audio driver"),
"type": "choice",
"advanced": True,
"choices": [
(_("Auto"), "auto"),
("ALSA", "alsa"),
("PulseAudio", "pulse"),
("OSS", "oss"),
],
"default": "auto",
"help": _(
"Which audio backend to use.\n"
"By default, Wine automatically picks the right one "
"for your system."
),
},
{
"option": "overrides",
"type": "mapping",
"label": _("DLL overrides"),
"help": _("Sets WINEDLLOVERRIDES when launching the game."),
},
{
"option": "show_debug",
"label": _("Output debugging info"),
"type": "choice",
"choices": [
(_("Disabled"), "-all"),
(_("Enabled"), ""),
(_("Inherit from environment"), "inherit"),
(_("Show FPS"), "+fps"),
(_("Full (CAUTION: Will cause MASSIVE slowdown)"), "+all"),
],
"default": "-all",
"help": _("Output debugging information in the game log "
"(might affect performance)"),
},
{
"option": "ShowCrashDialog",
"label": _("Show crash dialogs"),
"type": "bool",
"default": False,
"advanced": True,
},
{
"option": "autoconf_joypad",
"type": "bool",
"label": _("Autoconfigure joypads"),
"advanced": True,
"default": False,
"help":
_("Automatically disables one of Wine's detected joypad "
"to avoid having 2 controllers detected"),
},
{
"option": "sandbox",
"type": "bool",
"label": _("Create a sandbox for Wine folders"),
"default": True,
"advanced": True,
"help": _(
"Do not use $HOME for desktop integration folders.\n"
"By default, it use the directories in the confined "
"Windows environment."
),
},
{
"option": "sandbox_dir",
"type": "directory_chooser",
"label": _("Sandbox directory"),
"help": _("Custom directory for desktop integration folders."),
"advanced": True,
},
]
@property
def context_menu_entries(self):
"""Return the contexual menu entries for wine"""
menu_entries = [("wineexec", _("Run EXE inside Wine prefix"), self.run_wineexec)]
if "Proton" not in self.get_version():
menu_entries.append(("winecfg", _("Wine configuration"), self.run_winecfg))
menu_entries += [
("wineshell", _("Open Bash terminal"), self.run_wine_terminal),
("wineconsole", _("Open Wine console"), self.run_wineconsole),
("wine-regedit", _("Wine registry"), self.run_regedit),
("winetricks", _("Winetricks"), self.run_winetricks),
("winecpl", _("Wine Control Panel"), self.run_winecpl),
]
return menu_entries
@property
def prefix_path(self):
"""Return the absolute path of the Wine prefix"""
_prefix_path = self.game_config.get("prefix") \
or os.environ.get("WINEPREFIX")
if not _prefix_path and self.game_config.get("exe"):
# Find prefix from game if we have one
_prefix_path = find_prefix(self.game_exe)
if not _prefix_path:
_prefix_path = DEFAULT_WINE_PREFIX
return os.path.expanduser(_prefix_path)
@property
def game_exe(self):
"""Return the game's executable's path, which may not exist. None
if there is no exe path defined."""
exe = self.game_config.get("exe")
if not exe:
logger.error("The game doesn't have an executable")
return None
if os.path.isabs(exe):
return system.fix_path_case(exe)
if not self.game_path:
logger.warning("The game has an executable, but not a game path")
return None
return system.fix_path_case(os.path.join(self.game_path, exe))
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
option = self.game_config.get("working_dir")
if option:
return option
if self.game_exe:
game_dir = os.path.dirname(self.game_exe)
if os.path.isdir(game_dir):
return game_dir
return super().working_dir
@property
def nvidia_shader_cache_path(self):
"""WINE should give each game its own shader cache if possible."""
return self.game_path or self.shader_cache_dir
@property
def wine_arch(self):
"""Return the wine architecture.
Get it from the config or detect it from the prefix"""
arch = self.game_config.get("arch") or "auto"
if arch not in ("win32", "win64"):
arch = detect_arch(self.prefix_path, self.get_executable())
return arch
def get_version(self, use_default=True):
"""Return the Wine version to use. use_default can be set to false to
force the installation of a specific wine version"""
runner_version = self.runner_config.get("version")
if runner_version:
return runner_version
if use_default:
return get_default_version()
def get_path_for_version(self, version):
"""Return the absolute path of a wine executable for a given version"""
if version in WINE_PATHS:
return system.find_executable(WINE_PATHS[version])
if "Proton" in version:
for proton_path in get_proton_paths():
if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
return os.path.join(proton_path, version, "dist/bin/wine")
if version.startswith("PlayOnLinux"):
version, arch = version.split()[1].rsplit("-", 1)
return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
if version == "custom":
return self.runner_config.get("custom_wine_path", "")
return os.path.join(WINE_DIR, version, "bin/wine")
def get_executable(self, version=None, fallback=True):
"""Return the path to the Wine executable.
A specific version can be specified if needed.
"""
if version is None:
version = self.get_version()
if not version:
return
wine_path = self.get_path_for_version(version)
if system.path_exists(wine_path):
return wine_path
if fallback:
# Fallback to default version
default_version = get_default_version()
wine_path = self.get_path_for_version(default_version)
if wine_path:
# Update the version in the config
if version == self.runner_config.get("version"):
self.runner_config["version"] = default_version
# TODO: runner_config is a dict so we have to instanciate a
# LutrisConfig object to save it.
# XXX: The version key could be either in the game specific
# config or the runner specific config. We need to know
# which one to get the correct LutrisConfig object.
return wine_path
def is_installed(self, version=None, fallback=True, min_version=None):
"""Check if Wine is installed.
If no version is passed, checks if any version of wine is available
"""
if version:
return system.path_exists(self.get_executable(version, fallback))
wine_versions = get_wine_versions()
if min_version:
min_version_list, _, _ = parse_version(min_version)
for wine_version in wine_versions:
version_list, _, _ = parse_version(wine_version)
if version_list > min_version_list:
return True
logger.warning("Wine %s or higher not found", min_version)
return bool(wine_versions)
@classmethod
def msi_exec(
cls,
msi_file,
quiet=False,
prefix=None,
wine_path=None,
working_dir=None,
blocking=False,
):
msi_args = "/i %s" % msi_file
if quiet:
msi_args += " /q"
return wineexec(
"msiexec",
args=msi_args,
prefix=prefix,
wine_path=wine_path,
working_dir=working_dir,
blocking=blocking,
)
def _run_executable(self, executable):
"""Runs a Windows executable using this game's configuration"""
wineexec(
executable,
wine_path=self.get_executable(),
prefix=self.prefix_path,
working_dir=self.prefix_path,
config=self,
env=self.get_env(os_env=True),
)
def run_wineexec(self, *args):
"""Ask the user for an arbitrary exe file to run in the game's prefix"""
dlg = FileDialog(_("Select an EXE or MSI file"), default_path=self.game_path)
filename = dlg.filename
if not filename:
return
self.prelaunch()
self._run_executable(filename)
def run_wineconsole(self, *args):
"""Runs wineconsole inside wine prefix."""
self.prelaunch()
self._run_executable("wineconsole")
def run_winecfg(self, *args):
"""Run winecfg in the current context"""
self.prelaunch()
winecfg(
wine_path=self.get_executable(),
prefix=self.prefix_path,
arch=self.wine_arch,
config=self,
env=self.get_env(os_env=True),
)
def run_regedit(self, *args):
"""Run regedit in the current context"""
self.prelaunch()
self._run_executable("regedit")
def run_wine_terminal(self, *args):
terminal = self.system_config.get("terminal_app")
open_wine_terminal(
terminal=terminal,
wine_path=self.get_executable(),
prefix=self.prefix_path,
env=self.get_env(os_env=True)
)
def run_winetricks(self, *args):
"""Run winetricks in the current context"""
self.prelaunch()
winetricks(
"", prefix=self.prefix_path, wine_path=self.get_executable(), config=self, env=self.get_env(os_env=True)
)
def run_winecpl(self, *args):
"""Execute Wine control panel."""
self.prelaunch()
self._run_executable("control")
def run_winekill(self, *args):
"""Runs wineserver -k."""
winekill(
self.prefix_path,
arch=self.wine_arch,
wine_path=self.get_executable(),
env=self.get_env(),
initial_pids=self.get_pids(),
)
return True
def set_regedit_keys(self):
"""Reset regedit keys according to config."""
prefix_manager = WinePrefixManager(self.prefix_path)
# Those options are directly changed with the prefix manager and skip
# any calls to regedit.
managed_keys = {
"ShowCrashDialog": prefix_manager.set_crash_dialogs,
"Desktop": prefix_manager.set_virtual_desktop,
"WineDesktop": prefix_manager.set_desktop_size,
}
for key, path in self.reg_keys.items():
value = self.runner_config.get(key) or "auto"
if not value or value == "auto" and key not in managed_keys:
prefix_manager.clear_registry_subkeys(path, key)
elif key in self.runner_config:
if key in managed_keys:
# Do not pass fallback 'auto' value to managed keys
if value == "auto":
value = None
managed_keys[key](value)
continue
# Convert numeric strings to integers so they are saved as dword
if value.isdigit():
value = int(value)
prefix_manager.set_registry_key(path, key, value)
# We always configure the DPI, because if the user turns off DPI scaling, but it
# had been on the only way to implement that is to save 96 DPI into the registry.
prefix_manager.set_dpi(self.get_dpi())
def get_dpi(self):
"""Return the DPI to be used by Wine; returns 96 to disable scaling,
as this is Window's unscaled default DPI."""
if bool(self.runner_config.get("Dpi")):
explicit_dpi = self.runner_config.get("ExplicitDpi")
if explicit_dpi == "auto":
explicit_dpi = None
try:
explicit_dpi = int(explicit_dpi)
except ValueError:
explicit_dpi = None
return explicit_dpi or get_default_dpi()
return 96
def setup_dlls(self, manager_class, enable, version):
"""Enable or disable DLLs"""
dll_manager = manager_class(
self.prefix_path,
arch=self.wine_arch,
version=version,
)
# manual version only sets the dlls to native
if dll_manager.version.lower() != "manual":
if enable:
dll_manager.enable()
else:
dll_manager.disable()
if enable:
for dll in dll_manager.managed_dlls:
# We have to make sure that the dll exists before setting it to native
if dll_manager.dll_exists(dll):
self.dll_overrides[dll] = "n"
def prelaunch(self):
if not system.path_exists(os.path.join(self.prefix_path, "user.reg")):
logger.warning("No valid prefix detected in %s, creating one...", self.prefix_path)
create_prefix(self.prefix_path, wine_path=self.get_executable(), arch=self.wine_arch)
prefix_manager = WinePrefixManager(self.prefix_path)
if self.runner_config.get("autoconf_joypad", False):
prefix_manager.configure_joypads()
self.sandbox(prefix_manager)
self.set_regedit_keys()
self.setup_dlls(
DXVKManager,
bool(self.runner_config.get("dxvk")),
self.runner_config.get("dxvk_version")
)
self.setup_dlls(
VKD3DManager,
bool(self.runner_config.get("vkd3d")),
self.runner_config.get("vkd3d_version")
)
self.setup_dlls(
DXVKNVAPIManager,
bool(self.runner_config.get("dxvk_nvapi")),
self.runner_config.get("dxvk_nvapi_version")
)
self.setup_dlls(
D3DExtrasManager,
bool(self.runner_config.get("d3d_extras")),
self.runner_config.get("d3d_extras_version")
)
self.setup_dlls(
dgvoodoo2Manager,
bool(self.runner_config.get("dgvoodoo2")),
self.runner_config.get("dgvoodoo2_version")
)
return True
def get_dll_overrides(self):
"""Return the DLLs overriden at runtime"""
try:
overrides = self.runner_config["overrides"]
except KeyError:
overrides = {}
if not isinstance(overrides, dict):
logger.warning("DLL overrides is not a mapping: %s", overrides)
overrides = {}
return overrides
def get_env(self, os_env=False):
"""Return environment variables used by the game"""
# Always false to runner.get_env, the default value
# of os_env is inverted in the wine class,
# the OS env is read later.
env = super().get_env(False)
if os_env:
env.update(os.environ.copy())
show_debug = self.runner_config.get("show_debug", "-all")
if show_debug != "inherit":
env["WINEDEBUG"] = show_debug
if show_debug == "-all":
env["DXVK_LOG_LEVEL"] = "none"
env["WINEARCH"] = self.wine_arch
env["WINE"] = self.get_executable()
env["WINE_MONO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "mono")
env["WINE_GECKO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "gecko")
if is_gstreamer_build(self.get_executable()):
path_64 = os.path.join(WINE_DIR, self.get_version(), "lib64/gstreamer-1.0/")
path_32 = os.path.join(WINE_DIR, self.get_version(), "lib/gstreamer-1.0/")
if os.path.exists(path_64) or os.path.exists(path_32):
env["GST_PLUGIN_SYSTEM_PATH_1_0"] = path_64 + ":" + path_32
if self.prefix_path:
env["WINEPREFIX"] = self.prefix_path
if not ("WINEESYNC" in env and env["WINEESYNC"] == "1"):
env["WINEESYNC"] = "1" if self.runner_config.get("esync") else "0"
if not ("WINEFSYNC" in env and env["WINEFSYNC"] == "1"):
env["WINEFSYNC"] = "1" if self.runner_config.get("fsync") else "0"
if self.runner_config.get("fsr"):
env["WINE_FULLSCREEN_FSR"] = "1"
if self.runner_config.get("dxvk_nvapi"):
env["DXVK_NVAPIHACK"] = "0"
if self.runner_config.get("battleye"):
env["PROTON_BATTLEYE_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "battleye_runtime")
overrides = self.get_dll_overrides()
if overrides:
self.dll_overrides.update(overrides)
env["WINEDLLOVERRIDES"] = get_overrides_env(self.dll_overrides)
return env
def get_runtime_env(self):
"""Return runtime environment variables with path to wine for Lutris builds"""
wine_path = self.get_executable()
wine_root = None
if WINE_DIR:
wine_root = os.path.dirname(os.path.dirname(wine_path))
for proton_path in get_proton_paths():
if proton_path in wine_path:
wine_root = os.path.dirname(os.path.dirname(wine_path))
return runtime.get_env(
version="Ubuntu-18.04",
prefer_system_libs=self.system_config.get("prefer_system_libs", True),
wine_path=wine_root,
)
def get_pids(self, wine_path=None):
"""Return a list of pids of processes using the current wine exe."""
if wine_path:
exe = wine_path
else:
exe = self.get_executable()
if not exe.startswith("/"):
exe = system.find_executable(exe)
pids = system.get_pids_using_file(exe)
if self.wine_arch == "win64" and os.path.basename(exe) == "wine":
pids = pids | system.get_pids_using_file(exe + "64")
# Add wineserver PIDs to the mix (at least one occurence of fuser not
# picking the games's PID from wine/wine64 but from wineserver for some
# unknown reason.
pids = pids | system.get_pids_using_file(os.path.join(os.path.dirname(exe), "wineserver"))
return pids
def sandbox(self, wine_prefix):
if self.runner_config.get("sandbox", True):
wine_prefix.desktop_integration(desktop_dir=self.runner_config.get("sandbox_dir"))
else:
wine_prefix.desktop_integration(restore=True)
def play(self): # pylint: disable=too-many-return-statements # noqa: C901
game_exe = self.game_exe
arguments = self.game_config.get("args", "")
launch_info = {"env": self.get_env(os_env=False)}
using_dxvk = self.runner_config.get("dxvk")
if using_dxvk:
# Set this to 1 to enable access to more RAM for 32bit applications
launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
if not is_vulkan_supported():
if not display_vulkan_error(True):
return {"error": "VULKAN_NOT_FOUND"}
if not game_exe or not system.path_exists(game_exe):
return {"error": "FILE_NOT_FOUND", "file": game_exe}
if launch_info["env"].get("WINEESYNC") == "1":
limit_set = is_esync_limit_set()
wine_ver = is_version_esync(self.get_executable())
if not limit_set and not wine_ver:
esync_display_version_warning(True)
esync_display_limit_warning()
return {"error": "ESYNC_LIMIT_NOT_SET"}
if not is_esync_limit_set():
esync_display_limit_warning()
return {"error": "ESYNC_LIMIT_NOT_SET"}
if not wine_ver:
if not esync_display_version_warning(True):
return {"error": "NON_ESYNC_WINE_VERSION"}
if launch_info["env"].get("WINEFSYNC") == "1":
fsync_supported = is_fsync_supported()
wine_ver = is_version_fsync(self.get_executable())
if not fsync_supported and not wine_ver:
fsync_display_version_warning(True)
fsync_display_support_warning()
return {"error": "FSYNC_NOT_SUPPORTED"}
if not fsync_supported:
fsync_display_support_warning()
return {"error": "FSYNC_NOT_SUPPORTED"}
if not wine_ver:
if not fsync_display_version_warning(True):
return {"error": "NON_FSYNC_WINE_VERSION"}
command = [self.get_executable()]
game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
command.append(game_exe)
if args:
command = command + args
if arguments:
for arg in split_arguments(arguments):
command.append(arg)
launch_info["command"] = command
return launch_info
def force_stop_game(self, game):
"""Kill WINE with kindness, or at least with -k. This seems to leave a process
alive for some reason, but the caller will detect this and SIGKILL it."""
self.run_winekill()
@staticmethod
def parse_wine_path(path, prefix_path=None):
"""Take a Windows path, return the corresponding Linux path."""
if not prefix_path:
prefix_path = os.path.expanduser("~/.wine")
path = path.replace("\\\\", "/").replace("\\", "/")
if path[1] == ":": # absolute path
drive = os.path.join(prefix_path, "dosdevices", path[:2].lower())
if os.path.islink(drive): # Try to resolve the path
drive = os.readlink(drive)
return os.path.join(drive, path[3:])
if path[0] == "/": # drive-relative path. C is as good a guess as any..
return os.path.join(prefix_path, "drive_c", path[1:])
# Relative path
return path
context_menu_entries
property
readonly
¶
Return the contexual menu entries for wine
core_processes
¶
description
¶
entry_point_option
¶
game_exe
property
readonly
¶
Return the game's executable's path, which may not exist. None if there is no exe path defined.
game_options
¶
human_name
¶
multiple_versions
¶
nvidia_shader_cache_path
property
readonly
¶
WINE should give each game its own shader cache if possible.
platforms
¶
prefix_path
property
readonly
¶
Return the absolute path of the Wine prefix
reg_keys
¶
reg_prefix
¶
wine_arch
property
readonly
¶
Return the wine architecture.
Get it from the config or detect it from the prefix
working_dir
property
readonly
¶
Return the working directory to use when running the game.
__init__(self, config=None)
special
¶
Source code in lutris/runners/wine.py
def __init__(self, config=None): # noqa: C901
super().__init__(config)
self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy() # we'll modify this, so we better copy it
def get_wine_version_choices():
version_choices = [(_("Custom (select executable below)"), "custom")]
labels = {
"winehq-devel": _("WineHQ Devel ({})"),
"winehq-staging": _("WineHQ Staging ({})"),
"wine-development": _("Wine Development ({})"),
"system": _("System ({})"),
}
versions = get_wine_versions()
for version in versions:
if version in labels:
version_number = get_wine_version(WINE_PATHS[version])
label = labels[version].format(version_number)
else:
label = version
version_choices.append((label, version))
return version_choices
def esync_limit_callback(widget, option, config):
limits_set = is_esync_limit_set()
wine_path = self.get_path_for_version(config["version"])
wine_ver = is_version_esync(wine_path)
response = True
if not wine_ver:
response = thread_safe_call(esync_display_version_warning)
if not limits_set:
thread_safe_call(esync_display_limit_warning)
response = False
return widget, option, response
def fsync_support_callback(widget, option, config):
fsync_supported = is_fsync_supported()
wine_path = self.get_path_for_version(config["version"])
wine_ver = is_version_fsync(wine_path)
response = True
if not wine_ver:
response = thread_safe_call(fsync_display_version_warning)
if not fsync_supported:
thread_safe_call(fsync_display_support_warning)
response = False
return widget, option, response
def dxvk_vulkan_callback(widget, option, config):
response = True
if not is_vulkan_supported():
if not thread_safe_call(display_vulkan_error):
response = False
return widget, option, response
self.runner_options = [
{
"option": "version",
"label": _("Wine version"),
"type": "choice",
"choices": get_wine_version_choices,
"default": get_default_version(),
"help": _(
"The version of Wine used to launch the game.\n"
"Using the last version is generally recommended, "
"but some games work better on older versions."
),
},
{
"option": "custom_wine_path",
"label": _("Custom Wine executable"),
"type": "file",
"advanced": True,
"help": _("The Wine executable to be used if you have "
'selected "Custom" as the Wine version.'),
},
{
"option": "system_winetricks",
"label": _("Use system winetricks"),
"type": "bool",
"default": False,
"advanced": True,
"help": _("Switch on to use /usr/bin/winetricks for winetricks."),
},
{
"option": "dxvk",
"label": _("Enable DXVK"),
"type": "extended_bool",
"callback": dxvk_vulkan_callback,
"callback_on": True,
"default": True,
"active": True,
"help": _(
"Use DXVK to "
"increase compatibility and performance in Direct3D 11, 10 "
"and 9 applications by translating their calls to Vulkan."),
},
{
"option": "dxvk_version",
"label": _("DXVK version"),
"advanced": True,
"type": "choice_with_entry",
"choices": DXVKManager().version_choices,
"default": DXVKManager().version,
},
{
"option": "vkd3d",
"label": _("Enable VKD3D"),
"type": "extended_bool",
"callback": dxvk_vulkan_callback,
"callback_on": True,
"default": True,
"active": True,
"help": _(
"Use VKD3D to enable support for Direct3D 12 "
"applications by translating their calls to Vulkan."),
},
{
"option": "vkd3d_version",
"label": _("VKD3D version"),
"advanced": True,
"type": "choice_with_entry",
"choices": VKD3DManager().version_choices,
"default": VKD3DManager().version,
},
{
"option": "d3d_extras",
"label": _("Enable D3D Extras"),
"type": "bool",
"default": True,
"advanced": True,
"help": _(
"Replace Wine's D3DX and D3DCOMPILER libraries with alternative ones. "
"Needed for proper functionality of DXVK with some games."
),
},
{
"option": "d3d_extras_version",
"label": _("D3D Extras version"),
"advanced": True,
"type": "choice_with_entry",
"choices": D3DExtrasManager().version_choices,
"default": D3DExtrasManager().version,
},
{
"option": "dxvk_nvapi",
"label": _("Enable DXVK-NVAPI / DLSS"),
"type": "bool",
"default": True,
"advanced": True,
"help": _(
"Enable emulation of Nvidia's NVAPI and add DLSS support, if available."
),
},
{
"option": "dxvk_nvapi_version",
"label": _("DXVK NVAPI version"),
"advanced": True,
"type": "choice_with_entry",
"choices": DXVKNVAPIManager().version_choices,
"default": DXVKNVAPIManager().version,
},
{
"option": "dgvoodoo2",
"label": _("Enable dgvoodoo2"),
"type": "bool",
"default": False,
"advanced": False,
"help": _(
"dgvoodoo2 is an alternative translation layer for rendering old games "
"that utilize D3D1-7 and Glide APIs. As it translates to D3D11, it's "
"recommended to use it in combination with DXVK. Only 32-bit apps are supported."
),
},
{
"option": "dgvoodoo2_version",
"label": _("dgvoodoo2 version"),
"advanced": True,
"type": "choice_with_entry",
"choices": dgvoodoo2Manager().version_choices,
"default": dgvoodoo2Manager().version,
},
{
"option": "esync",
"label": _("Enable Esync"),
"type": "extended_bool",
"callback": esync_limit_callback,
"callback_on": True,
"active": True,
"default": True,
"help": _(
"Enable eventfd-based synchronization (esync). "
"This will increase performance in applications "
"that take advantage of multi-core processors."
),
},
{
"option": "fsync",
"label": _("Enable Fsync"),
"type": "extended_bool",
"default": True,
"callback": fsync_support_callback,
"callback_on": True,
"active": True,
"help": _(
"Enable futex-based synchronization (fsync). "
"This will increase performance in applications "
"that take advantage of multi-core processors. "
"Requires a custom kernel with the fsync patchset."
),
},
{
"option": "fsr",
"label": _("Enable AMD FidelityFX Super Resolution (FSR)"),
"type": "bool",
"default": True,
"help": _(
"Use FSR to upscale the game window to native resolution.\n"
"Requires Lutris Wine FShack >= 6.13 and setting the game to a lower resolution.\n"
"Does not work with games running in borderless window mode or that perform their own upscaling."
),
},
{
"option": "battleye",
"label": _("Enable BattlEye Anti-Cheat"),
"type": "bool",
"default": False,
"help": _(
"Enable support for BattlEye Anti-Cheat in supported games\n"
"Requires Lutris Wine 6.21-2 and newer or any other compatible Wine build.\n"
),
},
{
"option": "Desktop",
"label": _("Windowed (virtual desktop)"),
"type": "bool",
"default": False,
"help": _(
"Run the whole Windows desktop in a window.\n"
"Otherwise, run it fullscreen.\n"
"This corresponds to Wine's Virtual Desktop option."
),
},
{
"option": "WineDesktop",
"label": _("Virtual desktop resolution"),
"type": "choice_with_entry",
"choices": DISPLAY_MANAGER.get_resolutions,
"help": _("The size of the virtual desktop in pixels."),
},
{
"option": "Dpi",
"label": _("Enable DPI Scaling"),
"type": "bool",
"default": False,
"help": _(
"Enables the Windows application's DPI scaling.\n"
"Otherwise, disables DPI scaling by using 96 DPI.\n"
"This corresponds to Wine's Screen Resolution option."
),
},
{
"option": "ExplicitDpi",
"label": _("DPI"),
"type": "string",
"help": _(
"The DPI to be used if 'Enable DPI Scaling' is turned on.\n"
"If blank or 'auto', Lutris will auto-detect this."
),
},
{
"option": "MouseWarpOverride",
"label": _("Mouse Warp Override"),
"type": "choice",
"choices": [
(_("Enable"), "enable"),
(_("Disable"), "disable"),
(_("Force"), "force"),
],
"default": "enable",
"advanced": True,
"help": _(
"Override the default mouse pointer warping behavior\n"
"<b>Enable</b>: (Wine default) warp the pointer when the "
"mouse is exclusively acquired \n"
"<b>Disable</b>: never warp the mouse pointer \n"
"<b>Force</b>: always warp the pointer"
),
},
{
"option": "Audio",
"label": _("Audio driver"),
"type": "choice",
"advanced": True,
"choices": [
(_("Auto"), "auto"),
("ALSA", "alsa"),
("PulseAudio", "pulse"),
("OSS", "oss"),
],
"default": "auto",
"help": _(
"Which audio backend to use.\n"
"By default, Wine automatically picks the right one "
"for your system."
),
},
{
"option": "overrides",
"type": "mapping",
"label": _("DLL overrides"),
"help": _("Sets WINEDLLOVERRIDES when launching the game."),
},
{
"option": "show_debug",
"label": _("Output debugging info"),
"type": "choice",
"choices": [
(_("Disabled"), "-all"),
(_("Enabled"), ""),
(_("Inherit from environment"), "inherit"),
(_("Show FPS"), "+fps"),
(_("Full (CAUTION: Will cause MASSIVE slowdown)"), "+all"),
],
"default": "-all",
"help": _("Output debugging information in the game log "
"(might affect performance)"),
},
{
"option": "ShowCrashDialog",
"label": _("Show crash dialogs"),
"type": "bool",
"default": False,
"advanced": True,
},
{
"option": "autoconf_joypad",
"type": "bool",
"label": _("Autoconfigure joypads"),
"advanced": True,
"default": False,
"help":
_("Automatically disables one of Wine's detected joypad "
"to avoid having 2 controllers detected"),
},
{
"option": "sandbox",
"type": "bool",
"label": _("Create a sandbox for Wine folders"),
"default": True,
"advanced": True,
"help": _(
"Do not use $HOME for desktop integration folders.\n"
"By default, it use the directories in the confined "
"Windows environment."
),
},
{
"option": "sandbox_dir",
"type": "directory_chooser",
"label": _("Sandbox directory"),
"help": _("Custom directory for desktop integration folders."),
"advanced": True,
},
]
force_stop_game(self, game)
¶
Kill WINE with kindness, or at least with -k. This seems to leave a process alive for some reason, but the caller will detect this and SIGKILL it.
Source code in lutris/runners/wine.py
def force_stop_game(self, game):
"""Kill WINE with kindness, or at least with -k. This seems to leave a process
alive for some reason, but the caller will detect this and SIGKILL it."""
self.run_winekill()
get_dll_overrides(self)
¶
Return the DLLs overriden at runtime
Source code in lutris/runners/wine.py
def get_dll_overrides(self):
"""Return the DLLs overriden at runtime"""
try:
overrides = self.runner_config["overrides"]
except KeyError:
overrides = {}
if not isinstance(overrides, dict):
logger.warning("DLL overrides is not a mapping: %s", overrides)
overrides = {}
return overrides
get_dpi(self)
¶
Return the DPI to be used by Wine; returns 96 to disable scaling, as this is Window's unscaled default DPI.
Source code in lutris/runners/wine.py
def get_dpi(self):
"""Return the DPI to be used by Wine; returns 96 to disable scaling,
as this is Window's unscaled default DPI."""
if bool(self.runner_config.get("Dpi")):
explicit_dpi = self.runner_config.get("ExplicitDpi")
if explicit_dpi == "auto":
explicit_dpi = None
try:
explicit_dpi = int(explicit_dpi)
except ValueError:
explicit_dpi = None
return explicit_dpi or get_default_dpi()
return 96
get_env(self, os_env=False)
¶
Return environment variables used by the game
Source code in lutris/runners/wine.py
def get_env(self, os_env=False):
"""Return environment variables used by the game"""
# Always false to runner.get_env, the default value
# of os_env is inverted in the wine class,
# the OS env is read later.
env = super().get_env(False)
if os_env:
env.update(os.environ.copy())
show_debug = self.runner_config.get("show_debug", "-all")
if show_debug != "inherit":
env["WINEDEBUG"] = show_debug
if show_debug == "-all":
env["DXVK_LOG_LEVEL"] = "none"
env["WINEARCH"] = self.wine_arch
env["WINE"] = self.get_executable()
env["WINE_MONO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "mono")
env["WINE_GECKO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "gecko")
if is_gstreamer_build(self.get_executable()):
path_64 = os.path.join(WINE_DIR, self.get_version(), "lib64/gstreamer-1.0/")
path_32 = os.path.join(WINE_DIR, self.get_version(), "lib/gstreamer-1.0/")
if os.path.exists(path_64) or os.path.exists(path_32):
env["GST_PLUGIN_SYSTEM_PATH_1_0"] = path_64 + ":" + path_32
if self.prefix_path:
env["WINEPREFIX"] = self.prefix_path
if not ("WINEESYNC" in env and env["WINEESYNC"] == "1"):
env["WINEESYNC"] = "1" if self.runner_config.get("esync") else "0"
if not ("WINEFSYNC" in env and env["WINEFSYNC"] == "1"):
env["WINEFSYNC"] = "1" if self.runner_config.get("fsync") else "0"
if self.runner_config.get("fsr"):
env["WINE_FULLSCREEN_FSR"] = "1"
if self.runner_config.get("dxvk_nvapi"):
env["DXVK_NVAPIHACK"] = "0"
if self.runner_config.get("battleye"):
env["PROTON_BATTLEYE_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "battleye_runtime")
overrides = self.get_dll_overrides()
if overrides:
self.dll_overrides.update(overrides)
env["WINEDLLOVERRIDES"] = get_overrides_env(self.dll_overrides)
return env
get_executable(self, version=None, fallback=True)
¶
Return the path to the Wine executable. A specific version can be specified if needed.
Source code in lutris/runners/wine.py
def get_executable(self, version=None, fallback=True):
"""Return the path to the Wine executable.
A specific version can be specified if needed.
"""
if version is None:
version = self.get_version()
if not version:
return
wine_path = self.get_path_for_version(version)
if system.path_exists(wine_path):
return wine_path
if fallback:
# Fallback to default version
default_version = get_default_version()
wine_path = self.get_path_for_version(default_version)
if wine_path:
# Update the version in the config
if version == self.runner_config.get("version"):
self.runner_config["version"] = default_version
# TODO: runner_config is a dict so we have to instanciate a
# LutrisConfig object to save it.
# XXX: The version key could be either in the game specific
# config or the runner specific config. We need to know
# which one to get the correct LutrisConfig object.
return wine_path
get_path_for_version(self, version)
¶
Return the absolute path of a wine executable for a given version
Source code in lutris/runners/wine.py
def get_path_for_version(self, version):
"""Return the absolute path of a wine executable for a given version"""
if version in WINE_PATHS:
return system.find_executable(WINE_PATHS[version])
if "Proton" in version:
for proton_path in get_proton_paths():
if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
return os.path.join(proton_path, version, "dist/bin/wine")
if version.startswith("PlayOnLinux"):
version, arch = version.split()[1].rsplit("-", 1)
return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
if version == "custom":
return self.runner_config.get("custom_wine_path", "")
return os.path.join(WINE_DIR, version, "bin/wine")
get_pids(self, wine_path=None)
¶
Return a list of pids of processes using the current wine exe.
Source code in lutris/runners/wine.py
def get_pids(self, wine_path=None):
"""Return a list of pids of processes using the current wine exe."""
if wine_path:
exe = wine_path
else:
exe = self.get_executable()
if not exe.startswith("/"):
exe = system.find_executable(exe)
pids = system.get_pids_using_file(exe)
if self.wine_arch == "win64" and os.path.basename(exe) == "wine":
pids = pids | system.get_pids_using_file(exe + "64")
# Add wineserver PIDs to the mix (at least one occurence of fuser not
# picking the games's PID from wine/wine64 but from wineserver for some
# unknown reason.
pids = pids | system.get_pids_using_file(os.path.join(os.path.dirname(exe), "wineserver"))
return pids
get_runtime_env(self)
¶
Return runtime environment variables with path to wine for Lutris builds
Source code in lutris/runners/wine.py
def get_runtime_env(self):
"""Return runtime environment variables with path to wine for Lutris builds"""
wine_path = self.get_executable()
wine_root = None
if WINE_DIR:
wine_root = os.path.dirname(os.path.dirname(wine_path))
for proton_path in get_proton_paths():
if proton_path in wine_path:
wine_root = os.path.dirname(os.path.dirname(wine_path))
return runtime.get_env(
version="Ubuntu-18.04",
prefer_system_libs=self.system_config.get("prefer_system_libs", True),
wine_path=wine_root,
)
get_version(self, use_default=True)
¶
Return the Wine version to use. use_default can be set to false to force the installation of a specific wine version
Source code in lutris/runners/wine.py
def get_version(self, use_default=True):
"""Return the Wine version to use. use_default can be set to false to
force the installation of a specific wine version"""
runner_version = self.runner_config.get("version")
if runner_version:
return runner_version
if use_default:
return get_default_version()
is_installed(self, version=None, fallback=True, min_version=None)
¶
Check if Wine is installed. If no version is passed, checks if any version of wine is available
Source code in lutris/runners/wine.py
def is_installed(self, version=None, fallback=True, min_version=None):
"""Check if Wine is installed.
If no version is passed, checks if any version of wine is available
"""
if version:
return system.path_exists(self.get_executable(version, fallback))
wine_versions = get_wine_versions()
if min_version:
min_version_list, _, _ = parse_version(min_version)
for wine_version in wine_versions:
version_list, _, _ = parse_version(wine_version)
if version_list > min_version_list:
return True
logger.warning("Wine %s or higher not found", min_version)
return bool(wine_versions)
msi_exec(msi_file, quiet=False, prefix=None, wine_path=None, working_dir=None, blocking=False)
classmethod
¶
Source code in lutris/runners/wine.py
@classmethod
def msi_exec(
cls,
msi_file,
quiet=False,
prefix=None,
wine_path=None,
working_dir=None,
blocking=False,
):
msi_args = "/i %s" % msi_file
if quiet:
msi_args += " /q"
return wineexec(
"msiexec",
args=msi_args,
prefix=prefix,
wine_path=wine_path,
working_dir=working_dir,
blocking=blocking,
)
parse_wine_path(path, prefix_path=None)
staticmethod
¶
Take a Windows path, return the corresponding Linux path.
Source code in lutris/runners/wine.py
@staticmethod
def parse_wine_path(path, prefix_path=None):
"""Take a Windows path, return the corresponding Linux path."""
if not prefix_path:
prefix_path = os.path.expanduser("~/.wine")
path = path.replace("\\\\", "/").replace("\\", "/")
if path[1] == ":": # absolute path
drive = os.path.join(prefix_path, "dosdevices", path[:2].lower())
if os.path.islink(drive): # Try to resolve the path
drive = os.readlink(drive)
return os.path.join(drive, path[3:])
if path[0] == "/": # drive-relative path. C is as good a guess as any..
return os.path.join(prefix_path, "drive_c", path[1:])
# Relative path
return path
play(self)
¶
Source code in lutris/runners/wine.py
def play(self): # pylint: disable=too-many-return-statements # noqa: C901
game_exe = self.game_exe
arguments = self.game_config.get("args", "")
launch_info = {"env": self.get_env(os_env=False)}
using_dxvk = self.runner_config.get("dxvk")
if using_dxvk:
# Set this to 1 to enable access to more RAM for 32bit applications
launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
if not is_vulkan_supported():
if not display_vulkan_error(True):
return {"error": "VULKAN_NOT_FOUND"}
if not game_exe or not system.path_exists(game_exe):
return {"error": "FILE_NOT_FOUND", "file": game_exe}
if launch_info["env"].get("WINEESYNC") == "1":
limit_set = is_esync_limit_set()
wine_ver = is_version_esync(self.get_executable())
if not limit_set and not wine_ver:
esync_display_version_warning(True)
esync_display_limit_warning()
return {"error": "ESYNC_LIMIT_NOT_SET"}
if not is_esync_limit_set():
esync_display_limit_warning()
return {"error": "ESYNC_LIMIT_NOT_SET"}
if not wine_ver:
if not esync_display_version_warning(True):
return {"error": "NON_ESYNC_WINE_VERSION"}
if launch_info["env"].get("WINEFSYNC") == "1":
fsync_supported = is_fsync_supported()
wine_ver = is_version_fsync(self.get_executable())
if not fsync_supported and not wine_ver:
fsync_display_version_warning(True)
fsync_display_support_warning()
return {"error": "FSYNC_NOT_SUPPORTED"}
if not fsync_supported:
fsync_display_support_warning()
return {"error": "FSYNC_NOT_SUPPORTED"}
if not wine_ver:
if not fsync_display_version_warning(True):
return {"error": "NON_FSYNC_WINE_VERSION"}
command = [self.get_executable()]
game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
command.append(game_exe)
if args:
command = command + args
if arguments:
for arg in split_arguments(arguments):
command.append(arg)
launch_info["command"] = command
return launch_info
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/wine.py
def prelaunch(self):
if not system.path_exists(os.path.join(self.prefix_path, "user.reg")):
logger.warning("No valid prefix detected in %s, creating one...", self.prefix_path)
create_prefix(self.prefix_path, wine_path=self.get_executable(), arch=self.wine_arch)
prefix_manager = WinePrefixManager(self.prefix_path)
if self.runner_config.get("autoconf_joypad", False):
prefix_manager.configure_joypads()
self.sandbox(prefix_manager)
self.set_regedit_keys()
self.setup_dlls(
DXVKManager,
bool(self.runner_config.get("dxvk")),
self.runner_config.get("dxvk_version")
)
self.setup_dlls(
VKD3DManager,
bool(self.runner_config.get("vkd3d")),
self.runner_config.get("vkd3d_version")
)
self.setup_dlls(
DXVKNVAPIManager,
bool(self.runner_config.get("dxvk_nvapi")),
self.runner_config.get("dxvk_nvapi_version")
)
self.setup_dlls(
D3DExtrasManager,
bool(self.runner_config.get("d3d_extras")),
self.runner_config.get("d3d_extras_version")
)
self.setup_dlls(
dgvoodoo2Manager,
bool(self.runner_config.get("dgvoodoo2")),
self.runner_config.get("dgvoodoo2_version")
)
return True
run_regedit(self, *args)
¶
Run regedit in the current context
Source code in lutris/runners/wine.py
def run_regedit(self, *args):
"""Run regedit in the current context"""
self.prelaunch()
self._run_executable("regedit")
run_wine_terminal(self, *args)
¶
Source code in lutris/runners/wine.py
def run_wine_terminal(self, *args):
terminal = self.system_config.get("terminal_app")
open_wine_terminal(
terminal=terminal,
wine_path=self.get_executable(),
prefix=self.prefix_path,
env=self.get_env(os_env=True)
)
run_winecfg(self, *args)
¶
Run winecfg in the current context
Source code in lutris/runners/wine.py
def run_winecfg(self, *args):
"""Run winecfg in the current context"""
self.prelaunch()
winecfg(
wine_path=self.get_executable(),
prefix=self.prefix_path,
arch=self.wine_arch,
config=self,
env=self.get_env(os_env=True),
)
run_wineconsole(self, *args)
¶
Runs wineconsole inside wine prefix.
Source code in lutris/runners/wine.py
def run_wineconsole(self, *args):
"""Runs wineconsole inside wine prefix."""
self.prelaunch()
self._run_executable("wineconsole")
run_winecpl(self, *args)
¶
Execute Wine control panel.
Source code in lutris/runners/wine.py
def run_winecpl(self, *args):
"""Execute Wine control panel."""
self.prelaunch()
self._run_executable("control")
run_wineexec(self, *args)
¶
Ask the user for an arbitrary exe file to run in the game's prefix
Source code in lutris/runners/wine.py
def run_wineexec(self, *args):
"""Ask the user for an arbitrary exe file to run in the game's prefix"""
dlg = FileDialog(_("Select an EXE or MSI file"), default_path=self.game_path)
filename = dlg.filename
if not filename:
return
self.prelaunch()
self._run_executable(filename)
run_winekill(self, *args)
¶
Runs wineserver -k.
Source code in lutris/runners/wine.py
def run_winekill(self, *args):
"""Runs wineserver -k."""
winekill(
self.prefix_path,
arch=self.wine_arch,
wine_path=self.get_executable(),
env=self.get_env(),
initial_pids=self.get_pids(),
)
return True
run_winetricks(self, *args)
¶
Run winetricks in the current context
Source code in lutris/runners/wine.py
def run_winetricks(self, *args):
"""Run winetricks in the current context"""
self.prelaunch()
winetricks(
"", prefix=self.prefix_path, wine_path=self.get_executable(), config=self, env=self.get_env(os_env=True)
)
sandbox(self, wine_prefix)
¶
Source code in lutris/runners/wine.py
def sandbox(self, wine_prefix):
if self.runner_config.get("sandbox", True):
wine_prefix.desktop_integration(desktop_dir=self.runner_config.get("sandbox_dir"))
else:
wine_prefix.desktop_integration(restore=True)
set_regedit_keys(self)
¶
Reset regedit keys according to config.
Source code in lutris/runners/wine.py
def set_regedit_keys(self):
"""Reset regedit keys according to config."""
prefix_manager = WinePrefixManager(self.prefix_path)
# Those options are directly changed with the prefix manager and skip
# any calls to regedit.
managed_keys = {
"ShowCrashDialog": prefix_manager.set_crash_dialogs,
"Desktop": prefix_manager.set_virtual_desktop,
"WineDesktop": prefix_manager.set_desktop_size,
}
for key, path in self.reg_keys.items():
value = self.runner_config.get(key) or "auto"
if not value or value == "auto" and key not in managed_keys:
prefix_manager.clear_registry_subkeys(path, key)
elif key in self.runner_config:
if key in managed_keys:
# Do not pass fallback 'auto' value to managed keys
if value == "auto":
value = None
managed_keys[key](value)
continue
# Convert numeric strings to integers so they are saved as dword
if value.isdigit():
value = int(value)
prefix_manager.set_registry_key(path, key, value)
# We always configure the DPI, because if the user turns off DPI scaling, but it
# had been on the only way to implement that is to save 96 DPI into the registry.
prefix_manager.set_dpi(self.get_dpi())
setup_dlls(self, manager_class, enable, version)
¶
Enable or disable DLLs
Source code in lutris/runners/wine.py
def setup_dlls(self, manager_class, enable, version):
"""Enable or disable DLLs"""
dll_manager = manager_class(
self.prefix_path,
arch=self.wine_arch,
version=version,
)
# manual version only sets the dlls to native
if dll_manager.version.lower() != "manual":
if enable:
dll_manager.enable()
else:
dll_manager.disable()
if enable:
for dll in dll_manager.managed_dlls:
# We have to make sure that the dll exists before setting it to native
if dll_manager.dll_exists(dll):
self.dll_overrides[dll] = "n"
yuzu
¶
yuzu (Runner)
¶
Source code in lutris/runners/yuzu.py
class yuzu(Runner):
human_name = _("Yuzu")
platforms = [_("Nintendo Switch")]
description = _("Nintendo Switch emulator")
runnable_alone = True
runner_executable = "yuzu/yuzu"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("ROM file"),
"help": _("The game data, commonly called a ROM image."),
}
]
runner_options = [
{
"option": "prod_keys",
"label": _("Encryption keys"),
"type": "file",
"help": _("File containing the encryption keys."),
}, {
"option": "title_keys",
"label": _("Title keys"),
"type": "file",
"help": _("File containing the title keys."),
}
]
@property
def yuzu_data_dir(self):
"""Return dir where Yuzu files lie."""
candidates = ("~/.local/share/yuzu", )
for candidate in candidates:
path = system.fix_path_case(os.path.join(os.path.expanduser(candidate), "nand"))
if path and system.path_exists(path):
return path[:-len("nand")]
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
def _update_key(self, key_type):
"""Update a keys file if set """
yuzu_data_dir = self.yuzu_data_dir
if not yuzu_data_dir:
logger.error("Yuzu data dir not set")
return
if key_type == "prod_keys":
key_loc = os.path.join(yuzu_data_dir, "keys/prod.keys")
elif key_type == "title_keys":
key_loc = os.path.join(yuzu_data_dir, "keys/title.keys")
else:
logger.error("Invalid keys type %s!", key_type)
return
key = self.runner_config.get(key_type)
if not key:
logger.debug("No %s file was set.", key_type)
return
if not system.path_exists(key):
logger.warning("Keys file %s does not exist!", key)
return
keys_dir = os.path.dirname(key_loc)
if not os.path.exists(keys_dir):
os.makedirs(keys_dir)
elif os.path.isfile(key_loc) and filecmp.cmp(key, key_loc):
# If the files are identical, don't do anything
return
copyfile(key, key_loc)
def prelaunch(self):
for key in ["prod_keys", "title_keys"]:
self._update_key(key_type=key)
return True
description
¶
game_options
¶
human_name
¶
platforms
¶
runnable_alone
¶
runner_executable
¶
runner_options
¶
yuzu_data_dir
property
readonly
¶
Return dir where Yuzu files lie.
play(self)
¶
Run the game.
Source code in lutris/runners/yuzu.py
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get("main_file") or ""
if not system.path_exists(rom):
return {"error": "FILE_NOT_FOUND", "file": rom}
arguments.append(rom)
return {"command": arguments}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/yuzu.py
def prelaunch(self):
for key in ["prod_keys", "title_keys"]:
self._update_key(key_type=key)
return True
zdoom
¶
zdoom (Runner)
¶
Source code in lutris/runners/zdoom.py
class zdoom(Runner):
# http://zdoom.org/wiki/Command_line_parameters
description = _("ZDoom DOOM Game Engine")
human_name = _("ZDoom")
platforms = [_("Linux")]
runner_executable = "zdoom/zdoom"
game_options = [
{
"option": "main_file",
"type": "file",
"label": _("WAD file"),
"help": _("The game data, commonly called a WAD file."),
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Command line arguments used when launching the game."),
},
{
"option": "files",
"type": "multiple",
"label": _("PWAD files"),
"help": _("Used to load one or more PWAD files which generally contain "
"user-created levels."),
},
{
"option": "warp",
"type": "string",
"label": _("Warp to map"),
"help": _("Starts the game on the given map."),
},
{
"option": "savedir",
"type": "directory_chooser",
"label": _("Save path"),
"help": _("User-specified path where save files should be located."),
},
]
runner_options = [
{
"option": "2",
"label": _("Pixel Doubling"),
"type": "bool",
"default": False
},
{
"option": "4",
"label": _("Pixel Quadrupling"),
"type": "bool",
"default": False
},
{
"option": "nostartup",
"label": _("Disable Startup Screens"),
"type": "bool",
"default": False,
},
{
"option": "skill",
"label": _("Skill"),
"type": "choice",
"default": "",
"choices": {
(_("None"), ""),
(_("I'm Too Young To Die (1)"), "1"),
(_("Hey, Not Too Rough (2)"), "2"),
(_("Hurt Me Plenty (3)"), "3"),
(_("Ultra-Violence (4)"), "4"),
(_("Nightmare! (5)"), "5"),
},
},
{
"option":
"config",
"label":
_("Config file"),
"type":
"file",
"help": _(
"Used to load a user-created configuration file. If specified, "
"the file must contain the wad directory list or launch will fail."
),
},
]
def get_executable(self):
executable = super().get_executable()
executable_dir = os.path.dirname(executable)
if not system.path_exists(executable_dir):
return executable
if not system.path_exists(executable):
gzdoom_executable = os.path.join(executable_dir, "gzdoom")
if system.path_exists(gzdoom_executable):
return gzdoom_executable
return executable
def prelaunch(self):
if not LINUX_SYSTEM.get_soundfonts():
logger.warning("FluidSynth is not installed, you might not have any music")
return True
@property
def working_dir(self):
wad = self.game_config.get("main_file")
if wad:
return os.path.dirname(os.path.expanduser(wad))
wad_files = self.game_config.get("files")
if wad_files:
return os.path.dirname(os.path.expanduser(wad_files[0]))
def play(self): # noqa: C901
command = [self.get_executable()]
resolution = self.runner_config.get("resolution")
if resolution:
if resolution == "desktop":
width, height = display.DISPLAY_MANAGER.get_current_resolution()
else:
width, height = resolution.split("x")
command.append("-width")
command.append(width)
command.append("-height")
command.append(height)
# Append any boolean options.
bool_options = ["2", "4", "nostartup"]
for option in bool_options:
if self.runner_config.get(option):
command.append("-%s" % option)
# Append the skill level.
skill = self.runner_config.get("skill")
if skill:
command.append("-skill")
command.append(skill)
# Append directory for configuration file, if provided.
config = self.runner_config.get("config")
if config:
command.append("-config")
command.append(config)
# Append the warp arguments.
warp = self.game_config.get("warp")
if warp:
command.append("-warp")
for warparg in warp.split(" "):
command.append(warparg)
# Append directory for save games, if provided.
savedir = self.game_config.get("savedir")
if savedir:
command.append("-savedir")
command.append(savedir)
# Append the wad file to load, if provided.
wad = self.game_config.get("main_file")
if wad:
command.append("-iwad")
command.append(wad)
# Append the pwad files to load, if provided.
files = self.game_config.get("files") or []
pwads = [f for f in files if f.lower().endswith(".wad") or f.lower().endswith(".pk3")]
deh = [f for f in files if f.lower().endswith(".deh")]
bex = [f for f in files if f.lower().endswith(".bex")]
if deh:
command.append("-deh")
command.append(deh[0])
if bex:
command.append("-bex")
command.append(bex[0])
if pwads:
command.append("-file")
for pwad in pwads:
command.append(pwad)
# Append additional arguments, if provided.
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
return {"command": command}
description
¶
game_options
¶
human_name
¶
platforms
¶
runner_executable
¶
runner_options
¶
working_dir
property
readonly
¶
Return the working directory to use when running the game.
get_executable(self)
¶
Source code in lutris/runners/zdoom.py
def get_executable(self):
executable = super().get_executable()
executable_dir = os.path.dirname(executable)
if not system.path_exists(executable_dir):
return executable
if not system.path_exists(executable):
gzdoom_executable = os.path.join(executable_dir, "gzdoom")
if system.path_exists(gzdoom_executable):
return gzdoom_executable
return executable
play(self)
¶
Source code in lutris/runners/zdoom.py
def play(self): # noqa: C901
command = [self.get_executable()]
resolution = self.runner_config.get("resolution")
if resolution:
if resolution == "desktop":
width, height = display.DISPLAY_MANAGER.get_current_resolution()
else:
width, height = resolution.split("x")
command.append("-width")
command.append(width)
command.append("-height")
command.append(height)
# Append any boolean options.
bool_options = ["2", "4", "nostartup"]
for option in bool_options:
if self.runner_config.get(option):
command.append("-%s" % option)
# Append the skill level.
skill = self.runner_config.get("skill")
if skill:
command.append("-skill")
command.append(skill)
# Append directory for configuration file, if provided.
config = self.runner_config.get("config")
if config:
command.append("-config")
command.append(config)
# Append the warp arguments.
warp = self.game_config.get("warp")
if warp:
command.append("-warp")
for warparg in warp.split(" "):
command.append(warparg)
# Append directory for save games, if provided.
savedir = self.game_config.get("savedir")
if savedir:
command.append("-savedir")
command.append(savedir)
# Append the wad file to load, if provided.
wad = self.game_config.get("main_file")
if wad:
command.append("-iwad")
command.append(wad)
# Append the pwad files to load, if provided.
files = self.game_config.get("files") or []
pwads = [f for f in files if f.lower().endswith(".wad") or f.lower().endswith(".pk3")]
deh = [f for f in files if f.lower().endswith(".deh")]
bex = [f for f in files if f.lower().endswith(".bex")]
if deh:
command.append("-deh")
command.append(deh[0])
if bex:
command.append("-bex")
command.append(bex[0])
if pwads:
command.append("-file")
for pwad in pwads:
command.append(pwad)
# Append additional arguments, if provided.
args = self.game_config.get("args") or ""
for arg in split_arguments(args):
command.append(arg)
return {"command": command}
prelaunch(self)
¶
Run actions before running the game, override this method in runners
Source code in lutris/runners/zdoom.py
def prelaunch(self):
if not LINUX_SYSTEM.get_soundfonts():
logger.warning("FluidSynth is not installed, you might not have any music")
return True
runtime
¶
Runtime handling module
DEFAULT_RUNTIME
¶
RUNTIME_DISABLED
¶
Runtime
¶
Class for manipulating runtime folders
Source code in lutris/runtime.py
class Runtime:
"""Class for manipulating runtime folders"""
def __init__(self, name, updater):
self.name = name
self.updater = updater
@property
def local_runtime_path(self):
"""Return the local path for the runtime folder"""
if not self.name:
return None
return os.path.join(settings.RUNTIME_DIR, self.name)
def get_updated_at(self):
"""Return the modification date of the runtime folder"""
if not system.path_exists(self.local_runtime_path):
return None
return time.gmtime(os.path.getmtime(self.local_runtime_path))
def set_updated_at(self):
"""Set the creation and modification time to now"""
if not system.path_exists(self.local_runtime_path):
logger.error("No local runtime path in %s", self.local_runtime_path)
return
os.utime(self.local_runtime_path)
def should_update(self, remote_updated_at):
"""Determine if the current runtime should be updated"""
local_updated_at = self.get_updated_at()
if not local_updated_at:
logger.warning("Runtime %s is not available locally", self.name)
return True
if local_updated_at and local_updated_at >= remote_updated_at:
return False
logger.debug(
"Runtime %s locally updated on %s, remote created on %s)",
self.name,
time.strftime("%c", local_updated_at),
time.strftime("%c", remote_updated_at),
)
return True
def should_update_component(self, filename, remote_modified_at):
"""Should an individual component be updated?"""
file_path = os.path.join(settings.RUNTIME_DIR, self.name, filename)
if not system.path_exists(file_path):
return True
locally_modified_at = time.gmtime(os.path.getmtime(file_path))
if locally_modified_at >= remote_modified_at:
return False
return True
def download(self, remote_runtime_info):
"""Downloads a runtime locally"""
url = remote_runtime_info["url"]
if not url:
return self.download_components()
remote_updated_at = remote_runtime_info["created_at"]
remote_updated_at = time.strptime(remote_updated_at[:remote_updated_at.find(".")], "%Y-%m-%dT%H:%M:%S")
if not self.should_update(remote_updated_at):
return None
archive_path = os.path.join(settings.RUNTIME_DIR, os.path.basename(url))
downloader = Downloader(url, archive_path, overwrite=True)
downloader.start()
GLib.timeout_add(100, self.check_download_progress, downloader)
return downloader
def download_component(self, component):
"""Download an individual file from a runtime item"""
file_path = os.path.join(settings.RUNTIME_DIR, self.name, component["filename"])
try:
http.download_file(component["url"], file_path)
except http.HTTPError as ex:
logger.error("Failed to download runtime component %s: %s", component, ex)
return
return file_path
def get_runtime_components(self):
"""Fetch runtime components from the API"""
request = http.Request(settings.RUNTIME_URL + "/" + self.name)
try:
response = request.get()
except http.HTTPError as ex:
logger.error("Failed to get components: %s", ex)
return []
if not response.json:
return []
return response.json.get("components", [])
def download_components(self):
"""Download a runtime item by individual components."""
components = self.get_runtime_components()
downloads = []
for component in components:
modified_at = time.strptime(
component["modified_at"][:component["modified_at"].find(".")], "%Y-%m-%dT%H:%M:%S"
)
if not self.should_update_component(component["filename"], modified_at):
continue
downloads.append(component)
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
future_downloads = {
executor.submit(self.download_component, component): component["filename"]
for component in downloads
}
for future in concurrent.futures.as_completed(future_downloads):
filename = future_downloads[future]
if not filename:
logger.warning("Failed to get %s", future)
def check_download_progress(self, downloader):
"""Call download.check_progress(), return True if download finished."""
if not downloader or downloader.state in [
downloader.CANCELLED,
downloader.ERROR,
]:
logger.debug("Runtime update interrupted")
return False
downloader.check_progress()
if downloader.state == downloader.COMPLETED:
self.on_downloaded(downloader.dest)
return False
return True
def on_downloaded(self, path):
"""Actions taken once a runtime is downloaded
Arguments:
path (str): local path to the runtime archive
"""
stats = os.stat(path)
if not stats.st_size:
logger.error("Download failed: file %s is empty, Deleting file.", path)
os.unlink(path)
self.updater.notify_finish(self)
return False
directory, _filename = os.path.split(path)
# Delete the existing runtime path
initial_path = os.path.join(directory, self.name)
system.remove_folder(initial_path)
# Extract the runtime archive
jobs.AsyncCall(extract_archive, self.on_extracted, path, settings.RUNTIME_DIR, merge_single=False)
return False
def on_extracted(self, result, error):
"""Callback method when a runtime has extracted"""
if error:
logger.error("Runtime update failed")
logger.error(error)
self.updater.notify_finish(self)
return False
archive_path, _destination_path = result
logger.debug("Deleting runtime archive %s", archive_path)
os.unlink(archive_path)
self.set_updated_at()
self.updater.notify_finish(self)
return False
local_runtime_path
property
readonly
¶
Return the local path for the runtime folder
__init__(self, name, updater)
special
¶
Source code in lutris/runtime.py
def __init__(self, name, updater):
self.name = name
self.updater = updater
check_download_progress(self, downloader)
¶
Call download.check_progress(), return True if download finished.
Source code in lutris/runtime.py
def check_download_progress(self, downloader):
"""Call download.check_progress(), return True if download finished."""
if not downloader or downloader.state in [
downloader.CANCELLED,
downloader.ERROR,
]:
logger.debug("Runtime update interrupted")
return False
downloader.check_progress()
if downloader.state == downloader.COMPLETED:
self.on_downloaded(downloader.dest)
return False
return True
download(self, remote_runtime_info)
¶
Downloads a runtime locally
Source code in lutris/runtime.py
def download(self, remote_runtime_info):
"""Downloads a runtime locally"""
url = remote_runtime_info["url"]
if not url:
return self.download_components()
remote_updated_at = remote_runtime_info["created_at"]
remote_updated_at = time.strptime(remote_updated_at[:remote_updated_at.find(".")], "%Y-%m-%dT%H:%M:%S")
if not self.should_update(remote_updated_at):
return None
archive_path = os.path.join(settings.RUNTIME_DIR, os.path.basename(url))
downloader = Downloader(url, archive_path, overwrite=True)
downloader.start()
GLib.timeout_add(100, self.check_download_progress, downloader)
return downloader
download_component(self, component)
¶
Download an individual file from a runtime item
Source code in lutris/runtime.py
def download_component(self, component):
"""Download an individual file from a runtime item"""
file_path = os.path.join(settings.RUNTIME_DIR, self.name, component["filename"])
try:
http.download_file(component["url"], file_path)
except http.HTTPError as ex:
logger.error("Failed to download runtime component %s: %s", component, ex)
return
return file_path
download_components(self)
¶
Download a runtime item by individual components.
Source code in lutris/runtime.py
def download_components(self):
"""Download a runtime item by individual components."""
components = self.get_runtime_components()
downloads = []
for component in components:
modified_at = time.strptime(
component["modified_at"][:component["modified_at"].find(".")], "%Y-%m-%dT%H:%M:%S"
)
if not self.should_update_component(component["filename"], modified_at):
continue
downloads.append(component)
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
future_downloads = {
executor.submit(self.download_component, component): component["filename"]
for component in downloads
}
for future in concurrent.futures.as_completed(future_downloads):
filename = future_downloads[future]
if not filename:
logger.warning("Failed to get %s", future)
get_runtime_components(self)
¶
Fetch runtime components from the API
Source code in lutris/runtime.py
def get_runtime_components(self):
"""Fetch runtime components from the API"""
request = http.Request(settings.RUNTIME_URL + "/" + self.name)
try:
response = request.get()
except http.HTTPError as ex:
logger.error("Failed to get components: %s", ex)
return []
if not response.json:
return []
return response.json.get("components", [])
get_updated_at(self)
¶
Return the modification date of the runtime folder
Source code in lutris/runtime.py
def get_updated_at(self):
"""Return the modification date of the runtime folder"""
if not system.path_exists(self.local_runtime_path):
return None
return time.gmtime(os.path.getmtime(self.local_runtime_path))
on_downloaded(self, path)
¶
Actions taken once a runtime is downloaded
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path |
str |
local path to the runtime archive |
required |
Source code in lutris/runtime.py
def on_downloaded(self, path):
"""Actions taken once a runtime is downloaded
Arguments:
path (str): local path to the runtime archive
"""
stats = os.stat(path)
if not stats.st_size:
logger.error("Download failed: file %s is empty, Deleting file.", path)
os.unlink(path)
self.updater.notify_finish(self)
return False
directory, _filename = os.path.split(path)
# Delete the existing runtime path
initial_path = os.path.join(directory, self.name)
system.remove_folder(initial_path)
# Extract the runtime archive
jobs.AsyncCall(extract_archive, self.on_extracted, path, settings.RUNTIME_DIR, merge_single=False)
return False
on_extracted(self, result, error)
¶
Callback method when a runtime has extracted
Source code in lutris/runtime.py
def on_extracted(self, result, error):
"""Callback method when a runtime has extracted"""
if error:
logger.error("Runtime update failed")
logger.error(error)
self.updater.notify_finish(self)
return False
archive_path, _destination_path = result
logger.debug("Deleting runtime archive %s", archive_path)
os.unlink(archive_path)
self.set_updated_at()
self.updater.notify_finish(self)
return False
set_updated_at(self)
¶
Set the creation and modification time to now
Source code in lutris/runtime.py
def set_updated_at(self):
"""Set the creation and modification time to now"""
if not system.path_exists(self.local_runtime_path):
logger.error("No local runtime path in %s", self.local_runtime_path)
return
os.utime(self.local_runtime_path)
should_update(self, remote_updated_at)
¶
Determine if the current runtime should be updated
Source code in lutris/runtime.py
def should_update(self, remote_updated_at):
"""Determine if the current runtime should be updated"""
local_updated_at = self.get_updated_at()
if not local_updated_at:
logger.warning("Runtime %s is not available locally", self.name)
return True
if local_updated_at and local_updated_at >= remote_updated_at:
return False
logger.debug(
"Runtime %s locally updated on %s, remote created on %s)",
self.name,
time.strftime("%c", local_updated_at),
time.strftime("%c", remote_updated_at),
)
return True
should_update_component(self, filename, remote_modified_at)
¶
Should an individual component be updated?
Source code in lutris/runtime.py
def should_update_component(self, filename, remote_modified_at):
"""Should an individual component be updated?"""
file_path = os.path.join(settings.RUNTIME_DIR, self.name, filename)
if not system.path_exists(file_path):
return True
locally_modified_at = time.gmtime(os.path.getmtime(file_path))
if locally_modified_at >= remote_modified_at:
return False
return True
RuntimeUpdater
¶
Class handling the runtime updates
Source code in lutris/runtime.py
class RuntimeUpdater:
"""Class handling the runtime updates"""
current_updates = 0
status_updater = None
def is_updating(self):
"""Return True if the update process is running"""
return self.current_updates > 0
def update(self):
"""Launch the update process"""
if RUNTIME_DISABLED:
logger.warning("Runtime disabled, not updating it.")
return 0
for remote_runtime in self._iter_remote_runtimes():
runtime = Runtime(remote_runtime["name"], self)
downloader = runtime.download(remote_runtime)
if downloader:
self.current_updates += 1
return self.current_updates
@staticmethod
def _iter_remote_runtimes():
request = http.Request(settings.RUNTIME_URL + "?enabled=1")
try:
response = request.get()
except http.HTTPError as ex:
logger.error("Failed to get runtimes: %s", ex)
return
runtimes = response.json or []
for runtime in runtimes:
# Skip 32bit runtimes on 64 bit systems except the main runtime
if (
runtime["architecture"] == "i386" and LINUX_SYSTEM.is_64_bit
and not runtime["name"].startswith(("Ubuntu", "lib32"))
):
logger.debug(
"Skipping runtime %s for %s",
runtime["name"],
runtime["architecture"],
)
continue
# Skip 64bit runtimes on 32 bit systems
if runtime["architecture"] == "x86_64" and not LINUX_SYSTEM.is_64_bit:
logger.debug(
"Skipping runtime %s for %s",
runtime["name"],
runtime["architecture"],
)
continue
yield runtime
def notify_finish(self, runtime):
"""A runtime has finished downloading"""
logger.debug("Runtime %s is now updated and available", runtime.name)
self.current_updates -= 1
if self.current_updates == 0:
logger.info("Runtime is fully updated.")
current_updates
¶
status_updater
¶
is_updating(self)
¶
Return True if the update process is running
Source code in lutris/runtime.py
def is_updating(self):
"""Return True if the update process is running"""
return self.current_updates > 0
notify_finish(self, runtime)
¶
A runtime has finished downloading
Source code in lutris/runtime.py
def notify_finish(self, runtime):
"""A runtime has finished downloading"""
logger.debug("Runtime %s is now updated and available", runtime.name)
self.current_updates -= 1
if self.current_updates == 0:
logger.info("Runtime is fully updated.")
update(self)
¶
Launch the update process
Source code in lutris/runtime.py
def update(self):
"""Launch the update process"""
if RUNTIME_DISABLED:
logger.warning("Runtime disabled, not updating it.")
return 0
for remote_runtime in self._iter_remote_runtimes():
runtime = Runtime(remote_runtime["name"], self)
downloader = runtime.download(remote_runtime)
if downloader:
self.current_updates += 1
return self.current_updates
get_env(version=None, prefer_system_libs=False, wine_path=None)
¶
Return a dict containing LD_LIBRARY_PATH env var
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
version |
str |
Version of the runtime to use, such as "Ubuntu-18.04" or "legacy" |
None |
prefer_system_libs |
bool |
Whether to prioritize system libs over runtime libs |
False |
wine_path |
str |
If you prioritize system libs, provide the path for a lutris wine build if one is being used. This allows Lutris to prioritize the wine libs over everything else. |
None |
Returns:
| Type | Description |
|---|---|
dict |
Source code in lutris/runtime.py
def get_env(version=None, prefer_system_libs=False, wine_path=None):
"""Return a dict containing LD_LIBRARY_PATH env var
Params:
version (str): Version of the runtime to use, such as "Ubuntu-18.04" or "legacy"
prefer_system_libs (bool): Whether to prioritize system libs over runtime libs
wine_path (str): If you prioritize system libs, provide the path for a lutris wine build
if one is being used. This allows Lutris to prioritize the wine libs
over everything else.
Returns:
dict
"""
library_path = ":".join(get_paths(version=version, prefer_system_libs=prefer_system_libs, wine_path=wine_path))
env = {}
if library_path:
env["LD_LIBRARY_PATH"] = library_path
network_tools_path = os.path.join(settings.RUNTIME_DIR, "network-tools")
env["PATH"] = "%s:%s" % (network_tools_path, os.environ["PATH"])
return env
get_paths(version=None, prefer_system_libs=True, wine_path=None)
¶
Return a list of paths containing the runtime libraries.
Source code in lutris/runtime.py
def get_paths(version=None, prefer_system_libs=True, wine_path=None):
"""Return a list of paths containing the runtime libraries."""
if not RUNTIME_DISABLED:
paths = get_runtime_paths(version=version, prefer_system_libs=prefer_system_libs, wine_path=wine_path)
else:
paths = []
# Put existing LD_LIBRARY_PATH at the end
if os.environ.get("LD_LIBRARY_PATH"):
paths.append(os.environ["LD_LIBRARY_PATH"])
return paths
get_runtime_paths(version=None, prefer_system_libs=True, wine_path=None)
¶
Return Lutris runtime paths
Source code in lutris/runtime.py
def get_runtime_paths(version=None, prefer_system_libs=True, wine_path=None):
"""Return Lutris runtime paths"""
version = version or DEFAULT_RUNTIME
lutris_runtime_path = "%s-i686" % version
runtime_paths = [
lutris_runtime_path,
"steam/i386/lib/i386-linux-gnu",
"steam/i386/lib",
"steam/i386/usr/lib/i386-linux-gnu",
"steam/i386/usr/lib",
]
if LINUX_SYSTEM.is_64_bit:
lutris_runtime_path = "%s-x86_64" % version
runtime_paths += [
lutris_runtime_path,
"steam/amd64/lib/x86_64-linux-gnu",
"steam/amd64/lib",
"steam/amd64/usr/lib/x86_64-linux-gnu",
"steam/amd64/usr/lib",
]
paths = []
if prefer_system_libs:
if wine_path:
paths += get_winelib_paths(wine_path)
paths += list(LINUX_SYSTEM.iter_lib_folders())
# Then resolve absolute paths for the runtime
paths += [os.path.join(settings.RUNTIME_DIR, path) for path in runtime_paths]
return paths
get_winelib_paths(wine_path)
¶
Return wine libraries path for a Lutris wine build
Source code in lutris/runtime.py
def get_winelib_paths(wine_path):
"""Return wine libraries path for a Lutris wine build"""
paths = []
# Prioritize libwine.so.1 for lutris builds
for winelib_path in ("lib", "lib64"):
winelib_fullpath = os.path.join(wine_path or "", winelib_path)
if system.path_exists(winelib_fullpath):
paths.append(winelib_fullpath)
return paths
scanners
special
¶
lutris
¶
detect_game_from_installer(game_folder, installer)
¶
Source code in lutris/scanners/lutris.py
def detect_game_from_installer(game_folder, installer):
try:
exe_path = installer["script"]["game"].get("exe")
except KeyError:
exe_path = installer["script"].get("exe")
if not exe_path:
return
exe_path = exe_path.replace("$GAMEDIR/", "")
full_path = os.path.join(game_folder, exe_path)
if os.path.exists(full_path):
return full_path
find_game(game_folder, api_game)
¶
Source code in lutris/scanners/lutris.py
def find_game(game_folder, api_game):
installers = get_game_installers(api_game["slug"])
for installer in installers:
full_path = detect_game_from_installer(game_folder, installer)
if full_path:
return full_path, installer
return None, None
find_game_folder(dirname, api_game, slugs_map)
¶
Source code in lutris/scanners/lutris.py
def find_game_folder(dirname, api_game, slugs_map):
if api_game["slug"] in slugs_map:
game_folder = os.path.join(dirname, slugs_map[api_game["slug"]])
if os.path.exists(game_folder):
return game_folder
for alias in api_game["aliases"]:
if alias["slug"] in slugs_map:
game_folder = os.path.join(dirname, slugs_map[alias["slug"]])
if os.path.exists(game_folder):
return game_folder
get_game_slugs_and_folders(dirname)
¶
Scan a directory for games previously installed with lutris
Source code in lutris/scanners/lutris.py
def get_game_slugs_and_folders(dirname):
"""Scan a directory for games previously installed with lutris"""
folders = os.listdir(dirname)
game_folders = {}
for folder in folders:
if not os.path.isdir(os.path.join(dirname, folder)):
continue
game_folders[slugify(folder)] = folder
return game_folders
get_used_directories()
¶
Source code in lutris/scanners/lutris.py
def get_used_directories():
directories = set()
for game in get_games():
if game['directory']:
directories.add(game['directory'])
return directories
install_game(installer, game_folder)
¶
Source code in lutris/scanners/lutris.py
def install_game(installer, game_folder):
interpreter = ScriptInterpreter(installer)
interpreter.target_path = game_folder
interpreter.installer.save()
scan_directory(dirname)
¶
Source code in lutris/scanners/lutris.py
def scan_directory(dirname):
slugs_map = get_game_slugs_and_folders(dirname)
directories = get_used_directories()
api_games = get_api_games(list(slugs_map.keys()))
slugs_seen = set()
slugs_installed = set()
for api_game in api_games:
if api_game["slug"] in slugs_seen:
continue
slugs_seen.add(api_game["slug"])
game_folder = find_game_folder(dirname, api_game, slugs_map)
if game_folder in directories:
slugs_installed.add(api_game["slug"])
continue
full_path, installer = find_game(game_folder, api_game)
if full_path:
logger.info("Found %s in %s", api_game["name"], full_path)
try:
install_game(installer, game_folder)
except MissingGameDependency as ex:
logger.error("Skipped %s: %s", api_game["name"], ex)
download_lutris_media(installer["game_slug"])
slugs_installed.add(api_game["slug"])
installed_map = {slug: folder for slug, folder in slugs_map.items() if slug in slugs_installed}
missing_map = {slug: folder for slug, folder in slugs_map.items() if slug not in slugs_installed}
return installed_map, missing_map
retroarch
¶
EXTRA_FLAGS
¶
ROM_FLAGS
¶
SCANNERS
¶
clean_rom_name(name)
¶
Remove known flags from ROM filename and apply formatting
Source code in lutris/scanners/retroarch.py
def clean_rom_name(name):
"""Remove known flags from ROM filename and apply formatting"""
for flag in ROM_FLAGS:
name = name.replace(" (%s)" % flag, "")
for flag in EXTRA_FLAGS:
name = name.replace("[%s]" % flag, "")
if ", The" in name:
name = "The %s" % name.replace(", The", "")
name = name.strip()
return name
scan_directory(dirname)
¶
Add a directory of ROMs as Lutris games
Source code in lutris/scanners/retroarch.py
def scan_directory(dirname):
"""Add a directory of ROMs as Lutris games"""
files = os.listdir(dirname)
folder_extensions = {os.path.splitext(filename)[1] for filename in files}
core_matches = {}
for core, core_data in RECOMMENDED_CORES.items():
for ext in core_data.get("extensions", []):
if ext in folder_extensions:
core_matches[ext] = core
added_games = []
for filename in files:
name, ext = os.path.splitext(filename)
if ext not in core_matches:
continue
logger.info("Importing '%s'", name)
slug = slugify(name)
core = core_matches[ext]
config = {
"game": {
"core": core_matches[ext],
"main_file": os.path.join(dirname, filename)
}
}
installer_slug = "%s-libretro-%s" % (slug, core)
existing_game = get_games(filters={"installer_slug": installer_slug})
if existing_game:
game = Game(existing_game[0]["id"])
game.remove()
configpath = write_game_config(slug, config)
game_id = add_game(
name=name,
runner="libretro",
slug=slug,
directory=dirname,
installed=1,
installer_slug=installer_slug,
configpath=configpath
)
added_games.append(game_id)
return added_games
services
special
¶
Service package
DEFAULT_SERVICES
¶
SERVICES
¶
WIP_SERVICES
¶
get_enabled_services()
¶
Source code in lutris/services/__init__.py
def get_enabled_services():
return {
key: _class for key, _class in SERVICES.items()
if settings.read_setting(key, section="services").lower() == "true"
}
get_services()
¶
Return a mapping of available services
Source code in lutris/services/__init__.py
def get_services():
"""Return a mapping of available services"""
_services = {
"lutris": LutrisService,
"xdg": XDGService,
"gog": GOGService,
"humblebundle": HumbleBundleService,
"egs": EpicGamesStoreService,
"origin": OriginService,
"ubisoft": UbisoftConnectService,
}
if LINUX_SYSTEM.has_steam:
_services["steam"] = SteamService
_services["steamwindows"] = SteamWindowsService
if system.path_exists(DOLPHIN_GAME_CACHE_FILE):
_services["dolphin"] = DolphinService
return _services
base
¶
Generic service utilities
PGA_DB
¶
AuthTokenExpired (Exception)
¶
Exception raised when a token is no longer valid
Source code in lutris/services/base.py
class AuthTokenExpired(Exception):
"""Exception raised when a token is no longer valid"""
BaseService (Object)
¶
Base class for local services
Source code in lutris/services/base.py
class BaseService(GObject.Object):
"""Base class for local services"""
id = NotImplemented
_matcher = None
has_extras = False
name = NotImplemented
icon = NotImplemented
online = False
local = False
drm_free = False # DRM free games can be added to Lutris from an existing install
client_installer = None # ID of a script needed to install the client used by the service
scripts = {} # Mapping of Javascript snippets to handle redirections during auth
medias = {}
extra_medias = {}
default_format = "icon"
__gsignals__ = {
"service-games-load": (GObject.SIGNAL_RUN_FIRST, None, ()),
"service-games-loaded": (GObject.SIGNAL_RUN_FIRST, None, ()),
"service-login": (GObject.SIGNAL_RUN_FIRST, None, ()),
"service-logout": (GObject.SIGNAL_RUN_FIRST, None, ()),
}
@property
def matcher(self):
if self._matcher:
return self._matcher
return self.id
def run(self):
"""Override this method to run a launcher"""
logger.warning("This service doesn't run anything")
def is_launchable(self):
return False
def reload(self):
"""Refresh the service's games"""
self.emit("service-games-load")
try:
self.wipe_game_cache()
self.load()
self.load_icons()
self.add_installed_games()
finally:
self.emit("service-games-loaded")
def load(self):
logger.warning("Load method not implemented")
def load_icons(self):
"""Download all game media from the service"""
all_medias = self.medias.copy()
all_medias.update(self.extra_medias)
# Download icons
for icon_type in all_medias:
service_media = all_medias[icon_type]()
media_urls = service_media.get_media_urls()
download_media(media_urls, service_media)
# Process icons
for icon_type in all_medias:
service_media = all_medias[icon_type]()
service_media.render()
def wipe_game_cache(self):
logger.debug("Deleting games from service-games for %s", self.id)
sql.db_delete(PGA_DB, "service_games", "service", self.id)
def get_update_installers(self, db_game):
return []
def generate_installer(self, db_game):
"""Used to generate an installer from the data returned from the services"""
return {}
def match_game(self, service_game, api_game):
"""Match a service game to a lutris game referenced by its slug"""
if not service_game:
return
sql.db_update(
PGA_DB,
"service_games",
{"lutris_slug": api_game["slug"]},
conditions={"appid": service_game["appid"], "service": self.id}
)
unmatched_lutris_games = get_games(
searches={"installer_slug": self.matcher},
filters={"slug": api_game["slug"]},
excludes={"service": self.id}
)
for game in unmatched_lutris_games:
logger.debug("Updating unmatched game %s", game)
sql.db_update(
PGA_DB,
"games",
{"service": self.id, "service_id": service_game["appid"]},
conditions={"id": game["id"]}
)
def match_games(self):
"""Matching of service games to lutris games"""
service_games = {
str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
}
lutris_games = api.get_api_games(list(service_games.keys()), service=self.id)
for lutris_game in lutris_games:
for provider_game in lutris_game["provider_games"]:
if provider_game["service"] != self.id:
continue
self.match_game(service_games.get(provider_game["slug"]), lutris_game)
unmatched_service_games = get_games(searches={"installer_slug": self.matcher}, excludes={"service": self.id})
for lutris_game in api.get_api_games(game_slugs=[g["slug"] for g in unmatched_service_games]):
for provider_game in lutris_game["provider_games"]:
if provider_game["service"] != self.id:
continue
self.match_game(service_games.get(provider_game["slug"]), lutris_game)
def match_existing_game(self, db_games, appid):
"""Checks if a game is already installed and populates the service info"""
for _game in db_games:
logger.debug("Matching %s with existing install: %s", appid, _game)
game = Game(_game["id"])
game.appid = appid
game.service = self.id
game.save()
service_game = ServiceGameCollection.get_game(self.id, appid)
sql.db_update(PGA_DB, "service_games", {"lutris_slug": game.slug}, {"id": service_game["id"]})
return game
def get_installers_from_api(self, appid):
"""Query the lutris API for an appid and get existing installers for the service"""
lutris_games = api.get_api_games([appid], service=self.id)
service_installers = []
if lutris_games:
lutris_game = lutris_games[0]
installers = get_game_installers(lutris_game["slug"])
for installer in installers:
if self.matcher in installer["version"].lower():
service_installers.append(installer)
return service_installers
def install(self, db_game, update=False):
"""Install a service game, or starts the installer of the game.
Args:
db_game (dict or str): Database fields of the game to add, or (for Lutris service only
the slug of the game.)
Returns:
str: The slug of the game that was installed, to be run. None if the game should not be
run now. Many installers start from here, but continue running after this returns;
they return None.
"""
appid = db_game["appid"]
logger.debug("Installing %s from service %s", appid, self.id)
# Local services (aka game libraries that don't require any type of online interaction) can
# be added without going through an install dialog.
if self.local:
return self.simple_install(db_game)
if update:
service_installers = self.get_update_installers(db_game)
else:
service_installers = self.get_installers_from_api(appid)
# Check if the game is not already installed
for service_installer in service_installers:
existing_game = self.match_existing_game(
get_games(filters={"installer_slug": service_installer["slug"], "installed": "1"}),
appid
)
if existing_game:
logger.debug("Found existing game, aborting install")
return
if update:
installer = None
else:
installer = self.generate_installer(db_game)
if installer:
if service_installers:
installer["version"] = installer["version"] + " (auto-generated)"
service_installers.append(installer)
if not service_installers:
logger.error("No installer found for %s", db_game)
return
application = Gio.Application.get_default()
application.show_installer_window(service_installers, service=self, appid=appid)
def simple_install(self, db_game):
"""A simplified version of the install method, used when a game doesn't need any setup"""
installer = self.generate_installer(db_game)
configpath = write_game_config(db_game["slug"], installer["script"])
game_id = add_game(
name=installer["name"],
runner=installer["runner"],
slug=installer["game_slug"],
directory=self.get_game_directory(installer),
installed=1,
installer_slug=installer["slug"],
configpath=configpath,
service=self.id,
service_id=db_game["appid"],
)
return game_id
def add_installed_games(self):
"""Services can implement this method to scan for locally
installed games and add them to lutris.
"""
def get_game_directory(self, _installer):
"""Specific services should implement this"""
return ""
client_installer
¶
default_format
¶
drm_free
¶
extra_medias
¶
has_extras
¶
icon
¶
id
¶
local
¶
matcher
property
readonly
¶
medias
¶
name
¶
online
¶
scripts
¶
add_installed_games(self)
¶
Services can implement this method to scan for locally installed games and add them to lutris.
Source code in lutris/services/base.py
def add_installed_games(self):
"""Services can implement this method to scan for locally
installed games and add them to lutris.
"""
generate_installer(self, db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/base.py
def generate_installer(self, db_game):
"""Used to generate an installer from the data returned from the services"""
return {}
get_game_directory(self, _installer)
¶
Specific services should implement this
Source code in lutris/services/base.py
def get_game_directory(self, _installer):
"""Specific services should implement this"""
return ""
get_installers_from_api(self, appid)
¶
Query the lutris API for an appid and get existing installers for the service
Source code in lutris/services/base.py
def get_installers_from_api(self, appid):
"""Query the lutris API for an appid and get existing installers for the service"""
lutris_games = api.get_api_games([appid], service=self.id)
service_installers = []
if lutris_games:
lutris_game = lutris_games[0]
installers = get_game_installers(lutris_game["slug"])
for installer in installers:
if self.matcher in installer["version"].lower():
service_installers.append(installer)
return service_installers
get_update_installers(self, db_game)
¶
Source code in lutris/services/base.py
def get_update_installers(self, db_game):
return []
install(self, db_game, update=False)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/base.py
def install(self, db_game, update=False):
"""Install a service game, or starts the installer of the game.
Args:
db_game (dict or str): Database fields of the game to add, or (for Lutris service only
the slug of the game.)
Returns:
str: The slug of the game that was installed, to be run. None if the game should not be
run now. Many installers start from here, but continue running after this returns;
they return None.
"""
appid = db_game["appid"]
logger.debug("Installing %s from service %s", appid, self.id)
# Local services (aka game libraries that don't require any type of online interaction) can
# be added without going through an install dialog.
if self.local:
return self.simple_install(db_game)
if update:
service_installers = self.get_update_installers(db_game)
else:
service_installers = self.get_installers_from_api(appid)
# Check if the game is not already installed
for service_installer in service_installers:
existing_game = self.match_existing_game(
get_games(filters={"installer_slug": service_installer["slug"], "installed": "1"}),
appid
)
if existing_game:
logger.debug("Found existing game, aborting install")
return
if update:
installer = None
else:
installer = self.generate_installer(db_game)
if installer:
if service_installers:
installer["version"] = installer["version"] + " (auto-generated)"
service_installers.append(installer)
if not service_installers:
logger.error("No installer found for %s", db_game)
return
application = Gio.Application.get_default()
application.show_installer_window(service_installers, service=self, appid=appid)
is_launchable(self)
¶
Source code in lutris/services/base.py
def is_launchable(self):
return False
load(self)
¶
Source code in lutris/services/base.py
def load(self):
logger.warning("Load method not implemented")
load_icons(self)
¶
Download all game media from the service
Source code in lutris/services/base.py
def load_icons(self):
"""Download all game media from the service"""
all_medias = self.medias.copy()
all_medias.update(self.extra_medias)
# Download icons
for icon_type in all_medias:
service_media = all_medias[icon_type]()
media_urls = service_media.get_media_urls()
download_media(media_urls, service_media)
# Process icons
for icon_type in all_medias:
service_media = all_medias[icon_type]()
service_media.render()
match_existing_game(self, db_games, appid)
¶
Checks if a game is already installed and populates the service info
Source code in lutris/services/base.py
def match_existing_game(self, db_games, appid):
"""Checks if a game is already installed and populates the service info"""
for _game in db_games:
logger.debug("Matching %s with existing install: %s", appid, _game)
game = Game(_game["id"])
game.appid = appid
game.service = self.id
game.save()
service_game = ServiceGameCollection.get_game(self.id, appid)
sql.db_update(PGA_DB, "service_games", {"lutris_slug": game.slug}, {"id": service_game["id"]})
return game
match_game(self, service_game, api_game)
¶
Match a service game to a lutris game referenced by its slug
Source code in lutris/services/base.py
def match_game(self, service_game, api_game):
"""Match a service game to a lutris game referenced by its slug"""
if not service_game:
return
sql.db_update(
PGA_DB,
"service_games",
{"lutris_slug": api_game["slug"]},
conditions={"appid": service_game["appid"], "service": self.id}
)
unmatched_lutris_games = get_games(
searches={"installer_slug": self.matcher},
filters={"slug": api_game["slug"]},
excludes={"service": self.id}
)
for game in unmatched_lutris_games:
logger.debug("Updating unmatched game %s", game)
sql.db_update(
PGA_DB,
"games",
{"service": self.id, "service_id": service_game["appid"]},
conditions={"id": game["id"]}
)
match_games(self)
¶
Matching of service games to lutris games
Source code in lutris/services/base.py
def match_games(self):
"""Matching of service games to lutris games"""
service_games = {
str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
}
lutris_games = api.get_api_games(list(service_games.keys()), service=self.id)
for lutris_game in lutris_games:
for provider_game in lutris_game["provider_games"]:
if provider_game["service"] != self.id:
continue
self.match_game(service_games.get(provider_game["slug"]), lutris_game)
unmatched_service_games = get_games(searches={"installer_slug": self.matcher}, excludes={"service": self.id})
for lutris_game in api.get_api_games(game_slugs=[g["slug"] for g in unmatched_service_games]):
for provider_game in lutris_game["provider_games"]:
if provider_game["service"] != self.id:
continue
self.match_game(service_games.get(provider_game["slug"]), lutris_game)
reload(self)
¶
Refresh the service's games
Source code in lutris/services/base.py
def reload(self):
"""Refresh the service's games"""
self.emit("service-games-load")
try:
self.wipe_game_cache()
self.load()
self.load_icons()
self.add_installed_games()
finally:
self.emit("service-games-loaded")
run(self)
¶
Override this method to run a launcher
Source code in lutris/services/base.py
def run(self):
"""Override this method to run a launcher"""
logger.warning("This service doesn't run anything")
simple_install(self, db_game)
¶
A simplified version of the install method, used when a game doesn't need any setup
Source code in lutris/services/base.py
def simple_install(self, db_game):
"""A simplified version of the install method, used when a game doesn't need any setup"""
installer = self.generate_installer(db_game)
configpath = write_game_config(db_game["slug"], installer["script"])
game_id = add_game(
name=installer["name"],
runner=installer["runner"],
slug=installer["game_slug"],
directory=self.get_game_directory(installer),
installed=1,
installer_slug=installer["slug"],
configpath=configpath,
service=self.id,
service_id=db_game["appid"],
)
return game_id
wipe_game_cache(self)
¶
Source code in lutris/services/base.py
def wipe_game_cache(self):
logger.debug("Deleting games from service-games for %s", self.id)
sql.db_delete(PGA_DB, "service_games", "service", self.id)
LutrisBanner (ServiceMedia)
¶
LutrisCoverart (ServiceMedia)
¶
LutrisCoverartMedium (LutrisCoverart)
¶
Source code in lutris/services/base.py
class LutrisCoverartMedium(LutrisCoverart):
size = (176, 234)
size
¶
LutrisIcon (LutrisBanner)
¶
OnlineService (BaseService)
¶
Base class for online gaming services
Source code in lutris/services/base.py
class OnlineService(BaseService):
"""Base class for online gaming services"""
online = True
cookies_path = NotImplemented
cache_path = NotImplemented
requires_login_page = False
@property
def credential_files(self):
"""Return a list of all files used for authentication"""
return [self.cookies_path]
def login(self, parent=None):
logger.debug("Connecting to %s", self.name)
dialog = WebConnectDialog(self, parent)
dialog.set_modal(True)
dialog.show()
def is_authenticated(self):
"""Return whether the service is authenticated"""
return all(system.path_exists(path) for path in self.credential_files)
def wipe_game_cache(self):
"""Wipe the game cache, allowing it to be reloaded"""
if self.cache_path:
logger.debug("Deleting %s cache %s", self.id, self.cache_path)
if os.path.isdir(self.cache_path):
shutil.rmtree(self.cache_path)
elif system.path_exists(self.cache_path):
os.remove(self.cache_path)
super().wipe_game_cache()
def logout(self):
"""Disconnect from the service by removing all credentials"""
self.wipe_game_cache()
for auth_file in self.credential_files:
try:
os.remove(auth_file)
except OSError:
logger.warning("Unable to remove %s", auth_file)
logger.debug("logged out from %s", self.id)
self.emit("service-logout")
def load_cookies(self):
"""Load cookies from disk"""
if not system.path_exists(self.cookies_path):
logger.warning("No cookies found in %s, please authenticate first", self.cookies_path)
return
cookiejar = WebkitCookieJar(self.cookies_path)
cookiejar.load()
return cookiejar
cache_path
¶
cookies_path
¶
credential_files
property
readonly
¶
Return a list of all files used for authentication
online
¶
requires_login_page
¶
is_authenticated(self)
¶
Return whether the service is authenticated
Source code in lutris/services/base.py
def is_authenticated(self):
"""Return whether the service is authenticated"""
return all(system.path_exists(path) for path in self.credential_files)
load_cookies(self)
¶
Load cookies from disk
Source code in lutris/services/base.py
def load_cookies(self):
"""Load cookies from disk"""
if not system.path_exists(self.cookies_path):
logger.warning("No cookies found in %s, please authenticate first", self.cookies_path)
return
cookiejar = WebkitCookieJar(self.cookies_path)
cookiejar.load()
return cookiejar
login(self, parent=None)
¶
Source code in lutris/services/base.py
def login(self, parent=None):
logger.debug("Connecting to %s", self.name)
dialog = WebConnectDialog(self, parent)
dialog.set_modal(True)
dialog.show()
logout(self)
¶
Disconnect from the service by removing all credentials
Source code in lutris/services/base.py
def logout(self):
"""Disconnect from the service by removing all credentials"""
self.wipe_game_cache()
for auth_file in self.credential_files:
try:
os.remove(auth_file)
except OSError:
logger.warning("Unable to remove %s", auth_file)
logger.debug("logged out from %s", self.id)
self.emit("service-logout")
wipe_game_cache(self)
¶
Wipe the game cache, allowing it to be reloaded
Source code in lutris/services/base.py
def wipe_game_cache(self):
"""Wipe the game cache, allowing it to be reloaded"""
if self.cache_path:
logger.debug("Deleting %s cache %s", self.id, self.cache_path)
if os.path.isdir(self.cache_path):
shutil.rmtree(self.cache_path)
elif system.path_exists(self.cache_path):
os.remove(self.cache_path)
super().wipe_game_cache()
battlenet
¶
Battle.net service. Not ready yet.
BattleNetService (OnlineService)
¶
Service class for Battle.net
Source code in lutris/services/battlenet.py
class BattleNetService(OnlineService):
"""Service class for Battle.net"""
id = "battlenet"
name = _("Battle.net")
icon = "battlenet"
medias = {}
region = "na"
@property
def oauth_url(self):
"""Return the URL used for OAuth sign in"""
if self.region == "cn":
return "https://www.battlenet.com.cn/oauth"
return "https://%s.battle.net/oauth" % self.region
@property
def api_url(self):
"""Main API endpoint"""
if self.region == "cn":
return "https://gateway.battlenet.com.cn"
return "https://%s.api.blizzard.com" % self.region
@property
def login_url(self):
"""Battle.net login URL"""
if self.region == "cn":
return "https://www.battlenet.com.cn/login/zh"
return "https://%s.battle.net/login/en" % self.region
bethesda
¶
Bethesda service. Not ready yet.
BethesdaService (OnlineService)
¶
dolphin
¶
DolphinBanner (ServiceMedia)
¶
DolphinGame (ServiceGame)
¶
Game for the Dolphin emulator
Source code in lutris/services/dolphin.py
class DolphinGame(ServiceGame):
"""Game for the Dolphin emulator"""
service = "dolphin"
runner = "dolphin"
installer_slug = "dolphin"
@classmethod
def new_from_cache(cls, cache_entry):
"""Create a service game from an entry from the Dolphin cache"""
service_game = cls()
service_game.name = cache_entry["internal_name"]
service_game.appid = str(cache_entry["game_id"])
service_game.slug = slugify(cache_entry["internal_name"])
service_game.icon = service_game.get_banner(cache_entry)
service_game.details = json.dumps({
"path": cache_entry["file_path"],
"platform": cache_entry["platform"][:-1]
})
return service_game
@staticmethod
def get_game_name(cache_entry):
names = cache_entry["long_names"]
name_index = 1 if len(names.keys()) > 1 else 0
return str(names[list(names.keys())[name_index]])
def get_banner(self, cache_entry):
banner = DolphinBanner()
banner_path = banner.get_absolute_path(self.appid)
if os.path.exists(banner_path):
return banner_path
(width, height), data = cache_entry["volume_banner"]
if data:
img = Image.frombytes("RGB", (width, height), data, "raw", ("BGRX"))
# 96x32 is a bit small, maybe 2x scale?
# img.resize((width * 2, height * 2))
img.save(banner_path)
return banner_path
return ""
installer_slug
¶
runner
¶
service
¶
get_banner(self, cache_entry)
¶
Source code in lutris/services/dolphin.py
def get_banner(self, cache_entry):
banner = DolphinBanner()
banner_path = banner.get_absolute_path(self.appid)
if os.path.exists(banner_path):
return banner_path
(width, height), data = cache_entry["volume_banner"]
if data:
img = Image.frombytes("RGB", (width, height), data, "raw", ("BGRX"))
# 96x32 is a bit small, maybe 2x scale?
# img.resize((width * 2, height * 2))
img.save(banner_path)
return banner_path
return ""
get_game_name(cache_entry)
staticmethod
¶
Source code in lutris/services/dolphin.py
@staticmethod
def get_game_name(cache_entry):
names = cache_entry["long_names"]
name_index = 1 if len(names.keys()) > 1 else 0
return str(names[list(names.keys())[name_index]])
new_from_cache(cache_entry)
classmethod
¶
Create a service game from an entry from the Dolphin cache
Source code in lutris/services/dolphin.py
@classmethod
def new_from_cache(cls, cache_entry):
"""Create a service game from an entry from the Dolphin cache"""
service_game = cls()
service_game.name = cache_entry["internal_name"]
service_game.appid = str(cache_entry["game_id"])
service_game.slug = slugify(cache_entry["internal_name"])
service_game.icon = service_game.get_banner(cache_entry)
service_game.details = json.dumps({
"path": cache_entry["file_path"],
"platform": cache_entry["platform"][:-1]
})
return service_game
DolphinService (BaseService)
¶
Source code in lutris/services/dolphin.py
class DolphinService(BaseService):
id = "dolphin"
icon = "dolphin"
name = _("Dolphin")
local = True
medias = {
"icon": DolphinBanner
}
def load(self):
if not system.path_exists(DOLPHIN_GAME_CACHE_FILE):
return
cache_reader = DolphinCacheReader()
dolphin_games = [DolphinGame.new_from_cache(game) for game in cache_reader.get_games()]
for game in dolphin_games:
game.save()
return dolphin_games
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
return {
"name": db_game["name"],
"version": "Dolphin",
"slug": db_game["slug"],
"game_slug": slugify(db_game["name"]),
"runner": "dolphin",
"script": {
"game": {
"main_file": details["path"],
"platform": details["platform"]
},
}
}
def get_game_directory(self, installer):
"""Pull install location from installer"""
return os.path.dirname(installer["script"]["game"]["main_file"])
icon
¶
id
¶
local
¶
medias
¶
name
¶
generate_installer(self, db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/dolphin.py
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
return {
"name": db_game["name"],
"version": "Dolphin",
"slug": db_game["slug"],
"game_slug": slugify(db_game["name"]),
"runner": "dolphin",
"script": {
"game": {
"main_file": details["path"],
"platform": details["platform"]
},
}
}
get_game_directory(self, installer)
¶
Pull install location from installer
Source code in lutris/services/dolphin.py
def get_game_directory(self, installer):
"""Pull install location from installer"""
return os.path.dirname(installer["script"]["game"]["main_file"])
load(self)
¶
Source code in lutris/services/dolphin.py
def load(self):
if not system.path_exists(DOLPHIN_GAME_CACHE_FILE):
return
cache_reader = DolphinCacheReader()
dolphin_games = [DolphinGame.new_from_cache(game) for game in cache_reader.get_games()]
for game in dolphin_games:
game.save()
return dolphin_games
egs
¶
Epic Games Store service
BANNER_SIZE
¶
BOX_ART_SIZE
¶
EGS_BANNERS_PATH
¶
EGS_BOX_ART_PATH
¶
EGS_GAME_ART_PATH
¶
EGS_GAME_BOX_PATH
¶
EGS_LOGO_PATH
¶
DieselGameBannerSmall (DieselGameBox)
¶
DieselGameBox (DieselGameBoxTall)
¶
EGS game box
Source code in lutris/services/egs.py
class DieselGameBox(DieselGameBoxTall):
"""EGS game box"""
size = (316, 178)
remote_size = size
min_logo_x = 300
min_logo_y = 150
dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box")
api_field = "DieselGameBox"
DieselGameBoxLogo (DieselGameMedia)
¶
EGS game box
Source code in lutris/services/egs.py
class DieselGameBoxLogo(DieselGameMedia):
"""EGS game box"""
size = (200, 100)
remote_size = size
file_pattern = "%s.png"
visible = False
dest_path = os.path.join(settings.CACHE_DIR, "egs/game_logo")
api_field = "DieselGameBoxLogo"
DieselGameBoxSmall (DieselGameBoxTall)
¶
DieselGameBoxTall (DieselGameMedia)
¶
EGS tall game box
Source code in lutris/services/egs.py
class DieselGameBoxTall(DieselGameMedia):
"""EGS tall game box"""
size = (200, 267)
remote_size = size
min_logo_x = 100
min_logo_y = 100
dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box_tall")
api_field = "DieselGameBoxTall"
def render(self):
for filename in os.listdir(self.dest_path):
self._render_filename(filename)
DieselGameMedia (ServiceMedia)
¶
Source code in lutris/services/egs.py
class DieselGameMedia(ServiceMedia):
service = "egs"
remote_size = (200, 267)
file_pattern = "%s.jpg"
min_logo_x = 300
min_logo_y = 150
def _render_filename(self, filename):
game_box_path = os.path.join(self.dest_path, filename)
logo_path = os.path.join(EGS_LOGO_PATH, filename.replace(".jpg", ".png"))
has_logo = os.path.exists(logo_path)
thumb_image = Image.open(game_box_path)
thumb_image = thumb_image.convert("RGBA")
thumb_image = thumbnail_image(thumb_image, self.remote_size)
if has_logo:
logo_image = Image.open(logo_path)
logo_image = logo_image.convert("RGBA")
logo_width, logo_height = logo_image.size
if logo_width > self.min_logo_x:
logo_image = logo_image.resize((self.min_logo_x, int(
logo_height * (self.min_logo_x / logo_width))), resample=Image.BICUBIC)
elif logo_height > self.min_logo_y:
logo_image = logo_image.resize(
(int(logo_width * (self.min_logo_y / logo_height)), self.min_logo_y), resample=Image.BICUBIC)
thumb_image = paste_overlay(thumb_image, logo_image)
thumb_path = os.path.join(self.dest_path, filename)
thumb_image = thumb_image.convert("RGB")
thumb_image.save(thumb_path)
def get_media_url(self, details):
for image in details.get("keyImages", []):
if image["type"] == self.api_field:
return image["url"] + "?w=%s&resize=1&h=%s" % (
self.remote_size[0],
self.remote_size[1]
)
file_pattern
¶
min_logo_x
¶
min_logo_y
¶
remote_size
¶
service
¶
get_media_url(self, details)
¶
Source code in lutris/services/egs.py
def get_media_url(self, details):
for image in details.get("keyImages", []):
if image["type"] == self.api_field:
return image["url"] + "?w=%s&resize=1&h=%s" % (
self.remote_size[0],
self.remote_size[1]
)
EGSGame (ServiceGame)
¶
Service game for Epic Games Store
Source code in lutris/services/egs.py
class EGSGame(ServiceGame):
"""Service game for Epic Games Store"""
service = "egs"
@classmethod
def new_from_api(cls, egs_game):
"""Convert an EGS game to a service game"""
service_game = cls()
service_game.appid = egs_game["appName"]
service_game.slug = slugify(egs_game["title"])
service_game.name = egs_game["title"]
service_game.details = json.dumps(egs_game)
return service_game
service
¶
new_from_api(egs_game)
classmethod
¶
Convert an EGS game to a service game
Source code in lutris/services/egs.py
@classmethod
def new_from_api(cls, egs_game):
"""Convert an EGS game to a service game"""
service_game = cls()
service_game.appid = egs_game["appName"]
service_game.slug = slugify(egs_game["title"])
service_game.name = egs_game["title"]
service_game.details = json.dumps(egs_game)
return service_game
EpicGamesStoreService (OnlineService)
¶
Service class for Epic Games Store
Source code in lutris/services/egs.py
class EpicGamesStoreService(OnlineService):
"""Service class for Epic Games Store"""
id = "egs"
name = _("Epic Games Store")
icon = "egs"
online = True
runner = "wine"
client_installer = "epic-games-store"
medias = {
"game_box_small": DieselGameBoxSmall,
"game_banner_small": DieselGameBannerSmall,
"game_box": DieselGameBox,
"box_tall": DieselGameBoxTall,
}
extra_medias = {
"logo": DieselGameBoxLogo,
}
default_format = "game_banner_small"
requires_login_page = True
cookies_path = os.path.join(settings.CACHE_DIR, ".egs.auth")
token_path = os.path.join(settings.CACHE_DIR, ".egs.token")
cache_path = os.path.join(settings.CACHE_DIR, "egs-library.json")
login_url = "https://www.epicgames.com/id/login?redirectUrl=https://www.epicgames.com/id/api/redirect"
redirect_uri = "https://www.epicgames.com/id/api/redirect"
oauth_url = 'https://account-public-service-prod03.ol.epicgames.com'
catalog_url = 'https://catalog-public-service-prod06.ol.epicgames.com'
library_url = 'https://library-service.live.use1a.on.epicgames.com'
is_loading = False
user_agent = (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live '
'UnrealEngine/4.23.0-14907503+++Portal+Release-Live '
'Chrome/84.0.4147.38 Safari/537.36'
)
def __init__(self):
super().__init__()
self.session = requests.session()
self.session.headers['User-Agent'] = self.user_agent
if os.path.exists(self.token_path):
with open(self.token_path, encoding='utf-8') as token_file:
self.session_data = json.loads(token_file.read())
else:
self.session_data = {}
@property
def http_basic_auth(self):
return requests.auth.HTTPBasicAuth(
'34a02cf8f4414e29b15921876da36f9a',
'daafbccc737745039dffe53d94fc76cf'
)
def run(self):
egs = get_game_by_field(self.client_installer, "slug")
egs_game = Game(egs["id"])
egs_game.emit("game-launch")
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
def is_connected(self):
return self.is_authenticated()
def login_callback(self, content):
"""Once the user logs in in a browser window, Epic redirects
to a page containing a Session ID which we can use to finish the authentication.
Store session ID and exchange token to auth file"""
logger.debug("Login to EGS successful")
logger.debug(content)
content_json = json.loads(content.decode())
session_id = content_json["sid"]
_session = requests.session()
_session.headers.update({
'X-Epic-Event-Action': 'login',
'X-Epic-Event-Category': 'login',
'X-Epic-Strategy-Flags': '',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent': self.user_agent
})
_session.get('https://www.epicgames.com/id/api/set-sid', params={'sid': session_id})
_session.get('https://www.epicgames.com/id/api/csrf')
response = _session.post(
'https://www.epicgames.com/id/api/exchange/generate',
headers={'X-XSRF-TOKEN': _session.cookies['XSRF-TOKEN']}
)
if response.status_code != 200:
logger.error("Failed to connec to EGS (Status %s): %s", response.status_code, response.json())
return
self.start_session(response.json()['code'])
self.emit("service-login")
def resume_session(self):
self.session.headers['Authorization'] = 'bearer %s' % self.session_data["access_token"]
response = self.session.get('%s/account/api/oauth/verify' % self.oauth_url)
if response.status_code >= 500:
response.raise_for_status()
response_content = response.json()
if 'errorMessage' in response_content:
raise RuntimeError(response_content)
return response_content
def start_session(self, exchange_code=None):
if exchange_code:
grant_type = 'exchange_code'
token = exchange_code
else:
grant_type = 'refresh_token'
token = self.session_data["refresh_token"]
response = self.session.post(
'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token',
data={
'grant_type': grant_type,
grant_type: token,
'token_type': 'eg1'
},
auth=self.http_basic_auth
)
if response.status_code >= 500:
response.raise_for_status()
response_content = response.json()
if 'error' in response_content:
raise RuntimeError(response_content)
with open(self.token_path, "w", encoding='utf-8') as auth_file:
auth_file.write(json.dumps(response_content, indent=2))
self.session_data = response_content
def get_game_details(self, asset):
namespace = asset["namespace"]
catalog_item_id = asset["catalogItemId"]
response = self.session.get(
'%s/catalog/api/shared/namespace/%s/bulk/items' % (self.catalog_url, namespace),
params={
"id": catalog_item_id,
"includeDLCDetails": True,
"includeMainGameDetails": True,
"country": "US",
"locale": "en"
}
)
response.raise_for_status()
# Merge the details with the initial asset to keep 'appName'
asset.update(response.json()[catalog_item_id])
return asset
def get_library(self):
self.resume_session()
response = self.session.get(
'%s/library/api/public/items' % self.library_url,
params={'includeMetadata': 'true'}
)
response.raise_for_status()
resData = response.json()
records = resData['records']
cursor = resData['responseMetadata'].get('nextCursor', None)
while cursor:
response = self.session.get(
'%s/library/api/public/items' % self.library_url,
params={'includeMetadata': 'true',
'cursor': cursor}
)
response.raise_for_status()
resData = response.json()
records.extend(resData['records'])
cursor = resData['responseMetadata'].get('nextCursor', None)
games = []
for record in records:
if record["namespace"] == "ue":
continue
game_details = self.get_game_details(record)
games.append(game_details)
return games
def load(self):
"""Load the list of games"""
if self.is_loading:
logger.warning("EGS games are already loading")
return
self.is_loading = True
try:
library = self.get_library()
except Exception as ex: # pylint=disable:broad-except
self.is_loading = False
logger.warning("EGS Token expired")
raise AuthTokenExpired from ex
egs_games = []
for game in library:
egs_game = EGSGame.new_from_api(game)
egs_game.save()
egs_games.append(egs_game)
self.is_loading = False
return egs_games
def install_from_egs(self, egs_game, manifest):
"""Create a new Lutris game based on an existing EGS install"""
app_name = manifest["AppName"]
logger.debug("Installing EGS game %s", app_name)
service_game = ServiceGameCollection.get_game("egs", app_name)
if not service_game:
logger.error("Aborting install, %s is not present in the game library.", app_name)
return
lutris_game_id = slugify(service_game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(app_name)
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner=egs_game["runner"],
slug=slugify(service_game["name"]),
directory=egs_game["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=app_name,
)
return game_id
def add_installed_games(self):
"""Scan an existing EGS install for games"""
egs_game = get_game_by_field("epic-games-store", "slug")
if not egs_game:
logger.error("EGS is not installed in Lutris")
return
egs_prefix = egs_game["directory"].split("drive_c")[0]
logger.info("EGS detected in %s", egs_prefix)
if not system.path_exists(os.path.join(egs_prefix, "drive_c")):
logger.error("Invalid install of EGS at %s", egs_prefix)
return
egs_launcher = EGSLauncher(egs_prefix)
for manifest in egs_launcher.iter_manifests():
self.install_from_egs(egs_game, manifest)
logger.debug("All EGS games imported")
def generate_installer(self, db_game, egs_db_game):
egs_game = Game(egs_db_game["id"])
egs_exe = egs_game.config.game_config["exe"]
if not os.path.isabs(egs_exe):
egs_exe = os.path.join(egs_game.config.game_config["prefix"], egs_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": get_launch_arguments(db_game["appid"]),
},
"installer": [
{"task": {
"name": "wineexec",
"executable": egs_exe,
"args": get_launch_arguments(db_game["appid"], "install"),
"prefix": egs_game.config.game_config["prefix"],
"description": (
"The Epic Game Store will now open. Please launch "
"the installation of %s then close the EGS client "
"once the game has been downloaded." % db_game["name"]
)
}}
]
}
}
def install(self, db_game):
egs_game = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not egs_game or not egs_game["installed"]:
logger.warning("EGS (%s) not installed", self.client_installer)
installers = get_installers(
game_slug=self.client_installer,
)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, egs_game)],
service=self,
appid=db_game["appid"]
)
cache_path
¶
catalog_url
¶
client_installer
¶
cookies_path
¶
default_format
¶
extra_medias
¶
http_basic_auth
property
readonly
¶
icon
¶
id
¶
is_loading
¶
library_url
¶
login_url
¶
medias
¶
name
¶
oauth_url
¶
online
¶
redirect_uri
¶
requires_login_page
¶
runner
¶
token_path
¶
user_agent
¶
__init__(self)
special
¶
Source code in lutris/services/egs.py
def __init__(self):
super().__init__()
self.session = requests.session()
self.session.headers['User-Agent'] = self.user_agent
if os.path.exists(self.token_path):
with open(self.token_path, encoding='utf-8') as token_file:
self.session_data = json.loads(token_file.read())
else:
self.session_data = {}
add_installed_games(self)
¶
Scan an existing EGS install for games
Source code in lutris/services/egs.py
def add_installed_games(self):
"""Scan an existing EGS install for games"""
egs_game = get_game_by_field("epic-games-store", "slug")
if not egs_game:
logger.error("EGS is not installed in Lutris")
return
egs_prefix = egs_game["directory"].split("drive_c")[0]
logger.info("EGS detected in %s", egs_prefix)
if not system.path_exists(os.path.join(egs_prefix, "drive_c")):
logger.error("Invalid install of EGS at %s", egs_prefix)
return
egs_launcher = EGSLauncher(egs_prefix)
for manifest in egs_launcher.iter_manifests():
self.install_from_egs(egs_game, manifest)
logger.debug("All EGS games imported")
generate_installer(self, db_game, egs_db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/egs.py
def generate_installer(self, db_game, egs_db_game):
egs_game = Game(egs_db_game["id"])
egs_exe = egs_game.config.game_config["exe"]
if not os.path.isabs(egs_exe):
egs_exe = os.path.join(egs_game.config.game_config["prefix"], egs_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": get_launch_arguments(db_game["appid"]),
},
"installer": [
{"task": {
"name": "wineexec",
"executable": egs_exe,
"args": get_launch_arguments(db_game["appid"], "install"),
"prefix": egs_game.config.game_config["prefix"],
"description": (
"The Epic Game Store will now open. Please launch "
"the installation of %s then close the EGS client "
"once the game has been downloaded." % db_game["name"]
)
}}
]
}
}
get_game_details(self, asset)
¶
Source code in lutris/services/egs.py
def get_game_details(self, asset):
namespace = asset["namespace"]
catalog_item_id = asset["catalogItemId"]
response = self.session.get(
'%s/catalog/api/shared/namespace/%s/bulk/items' % (self.catalog_url, namespace),
params={
"id": catalog_item_id,
"includeDLCDetails": True,
"includeMainGameDetails": True,
"country": "US",
"locale": "en"
}
)
response.raise_for_status()
# Merge the details with the initial asset to keep 'appName'
asset.update(response.json()[catalog_item_id])
return asset
get_library(self)
¶
Source code in lutris/services/egs.py
def get_library(self):
self.resume_session()
response = self.session.get(
'%s/library/api/public/items' % self.library_url,
params={'includeMetadata': 'true'}
)
response.raise_for_status()
resData = response.json()
records = resData['records']
cursor = resData['responseMetadata'].get('nextCursor', None)
while cursor:
response = self.session.get(
'%s/library/api/public/items' % self.library_url,
params={'includeMetadata': 'true',
'cursor': cursor}
)
response.raise_for_status()
resData = response.json()
records.extend(resData['records'])
cursor = resData['responseMetadata'].get('nextCursor', None)
games = []
for record in records:
if record["namespace"] == "ue":
continue
game_details = self.get_game_details(record)
games.append(game_details)
return games
install(self, db_game)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/egs.py
def install(self, db_game):
egs_game = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not egs_game or not egs_game["installed"]:
logger.warning("EGS (%s) not installed", self.client_installer)
installers = get_installers(
game_slug=self.client_installer,
)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, egs_game)],
service=self,
appid=db_game["appid"]
)
install_from_egs(self, egs_game, manifest)
¶
Create a new Lutris game based on an existing EGS install
Source code in lutris/services/egs.py
def install_from_egs(self, egs_game, manifest):
"""Create a new Lutris game based on an existing EGS install"""
app_name = manifest["AppName"]
logger.debug("Installing EGS game %s", app_name)
service_game = ServiceGameCollection.get_game("egs", app_name)
if not service_game:
logger.error("Aborting install, %s is not present in the game library.", app_name)
return
lutris_game_id = slugify(service_game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(app_name)
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner=egs_game["runner"],
slug=slugify(service_game["name"]),
directory=egs_game["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=app_name,
)
return game_id
is_connected(self)
¶
Source code in lutris/services/egs.py
def is_connected(self):
return self.is_authenticated()
is_launchable(self)
¶
Source code in lutris/services/egs.py
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
load(self)
¶
Load the list of games
Source code in lutris/services/egs.py
def load(self):
"""Load the list of games"""
if self.is_loading:
logger.warning("EGS games are already loading")
return
self.is_loading = True
try:
library = self.get_library()
except Exception as ex: # pylint=disable:broad-except
self.is_loading = False
logger.warning("EGS Token expired")
raise AuthTokenExpired from ex
egs_games = []
for game in library:
egs_game = EGSGame.new_from_api(game)
egs_game.save()
egs_games.append(egs_game)
self.is_loading = False
return egs_games
login_callback(self, content)
¶
Once the user logs in in a browser window, Epic redirects to a page containing a Session ID which we can use to finish the authentication. Store session ID and exchange token to auth file
Source code in lutris/services/egs.py
def login_callback(self, content):
"""Once the user logs in in a browser window, Epic redirects
to a page containing a Session ID which we can use to finish the authentication.
Store session ID and exchange token to auth file"""
logger.debug("Login to EGS successful")
logger.debug(content)
content_json = json.loads(content.decode())
session_id = content_json["sid"]
_session = requests.session()
_session.headers.update({
'X-Epic-Event-Action': 'login',
'X-Epic-Event-Category': 'login',
'X-Epic-Strategy-Flags': '',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent': self.user_agent
})
_session.get('https://www.epicgames.com/id/api/set-sid', params={'sid': session_id})
_session.get('https://www.epicgames.com/id/api/csrf')
response = _session.post(
'https://www.epicgames.com/id/api/exchange/generate',
headers={'X-XSRF-TOKEN': _session.cookies['XSRF-TOKEN']}
)
if response.status_code != 200:
logger.error("Failed to connec to EGS (Status %s): %s", response.status_code, response.json())
return
self.start_session(response.json()['code'])
self.emit("service-login")
resume_session(self)
¶
Source code in lutris/services/egs.py
def resume_session(self):
self.session.headers['Authorization'] = 'bearer %s' % self.session_data["access_token"]
response = self.session.get('%s/account/api/oauth/verify' % self.oauth_url)
if response.status_code >= 500:
response.raise_for_status()
response_content = response.json()
if 'errorMessage' in response_content:
raise RuntimeError(response_content)
return response_content
run(self)
¶
Override this method to run a launcher
Source code in lutris/services/egs.py
def run(self):
egs = get_game_by_field(self.client_installer, "slug")
egs_game = Game(egs["id"])
egs_game.emit("game-launch")
start_session(self, exchange_code=None)
¶
Source code in lutris/services/egs.py
def start_session(self, exchange_code=None):
if exchange_code:
grant_type = 'exchange_code'
token = exchange_code
else:
grant_type = 'refresh_token'
token = self.session_data["refresh_token"]
response = self.session.post(
'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token',
data={
'grant_type': grant_type,
grant_type: token,
'token_type': 'eg1'
},
auth=self.http_basic_auth
)
if response.status_code >= 500:
response.raise_for_status()
response_content = response.json()
if 'error' in response_content:
raise RuntimeError(response_content)
with open(self.token_path, "w", encoding='utf-8') as auth_file:
auth_file.write(json.dumps(response_content, indent=2))
self.session_data = response_content
get_launch_arguments(app_name, action='launch')
¶
Source code in lutris/services/egs.py
def get_launch_arguments(app_name, action="launch"):
return (
"-opengl"
" -SkipBuildPatchPrereq"
" -com.epicgames.launcher://apps/%s?action=%s"
) % (app_name, action)
gog
¶
Module for handling the GOG service
GOGGame (ServiceGame)
¶
Representation of a GOG game
Source code in lutris/services/gog.py
class GOGGame(ServiceGame):
"""Representation of a GOG game"""
service = "gog"
@classmethod
def new_from_gog_game(cls, gog_game):
"""Return a GOG game instance from the API info"""
service_game = GOGGame()
service_game.appid = str(gog_game["id"])
service_game.slug = gog_game["slug"]
service_game.name = gog_game["title"]
service_game.details = json.dumps(gog_game)
return service_game
service
¶
new_from_gog_game(gog_game)
classmethod
¶
Return a GOG game instance from the API info
Source code in lutris/services/gog.py
@classmethod
def new_from_gog_game(cls, gog_game):
"""Return a GOG game instance from the API info"""
service_game = GOGGame()
service_game.appid = str(gog_game["id"])
service_game.slug = gog_game["slug"]
service_game.name = gog_game["title"]
service_game.details = json.dumps(gog_game)
return service_game
GOGService (OnlineService)
¶
Service class for GOG
Source code in lutris/services/gog.py
class GOGService(OnlineService):
"""Service class for GOG"""
id = "gog"
name = _("GOG")
icon = "gog"
has_extras = True
drm_free = True
medias = {
"banner_small": GogSmallBanner,
"banner": GogMediumBanner,
"banner_large": GogLargeBanner
}
default_format = "banner"
embed_url = "https://embed.gog.com"
api_url = "https://api.gog.com"
client_id = "46899977096215655"
client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"
redirect_uri = "https://embed.gog.com/on_login_success?origin=client"
login_success_url = "https://www.gog.com/on_login_success"
cookies_path = os.path.join(settings.CACHE_DIR, ".gog.auth")
token_path = os.path.join(settings.CACHE_DIR, ".gog.token")
cache_path = os.path.join(settings.CACHE_DIR, "gog-library.json")
is_loading = False
def __init__(self):
super().__init__()
self.selected_extras = None
gog_locales = {
"en": "en-US",
"de": "de-DE",
"fr": "fr-FR",
"pl": "pl-PL",
"ru": "ru-RU",
"zh": "zh-Hans",
}
self.locale = gog_locales.get(i18n.get_lang(), "en-US")
@property
def login_url(self):
"""Return authentication URL"""
params = {
"client_id": self.client_id,
"layout": "client2",
"redirect_uri": self.redirect_uri,
"response_type": "code",
}
return "https://auth.gog.com/auth?" + urlencode(params)
@property
def credential_files(self):
return [self.cookies_path, self.token_path]
def is_connected(self):
"""Return whether the user is authenticated and if the service is available"""
if not self.is_authenticated():
return False
try:
user_data = self.get_user_data()
except UnauthorizedAccess:
logger.warning("GOG token is invalid")
return False
return user_data and "username" in user_data
def load(self):
"""Load the user game library from the GOG API"""
if self.is_loading:
logger.warning("GOG games are already loading")
return
if not self.is_connected():
logger.error("User not connected to GOG")
return
self.is_loading = True
games = [GOGGame.new_from_gog_game(game) for game in self.get_library()]
for game in games:
game.save()
self.match_games()
self.is_loading = False
return games
def login_callback(self, url):
return self.request_token(url)
def request_token(self, url="", refresh_token=""):
"""Get authentication token from GOG"""
if refresh_token:
grant_type = "refresh_token"
extra_params = {"refresh_token": refresh_token}
else:
grant_type = "authorization_code"
parsed_url = urlparse(url)
response_params = dict(parse_qsl(parsed_url.query))
if "code" not in response_params:
logger.error("code not received from GOG")
logger.error(response_params)
return
extra_params = {
"code": response_params["code"],
"redirect_uri": self.redirect_uri,
}
params = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": grant_type,
}
params.update(extra_params)
url = "https://auth.gog.com/token?" + urlencode(params)
request = Request(url)
try:
request.get()
except HTTPError:
logger.error("Failed to get token, check your GOG credentials.")
logger.warning("Clearing existing credentials")
self.logout()
return
token = request.json
with open(self.token_path, "w", encoding='utf-8') as token_file:
token_file.write(json.dumps(token))
if not refresh_token:
self.emit("service-login")
def load_token(self):
"""Load token from disk"""
if not os.path.exists(self.token_path):
raise AuthenticationError("No GOG token available")
with open(self.token_path, encoding='utf-8') as token_file:
token_content = json.loads(token_file.read())
return token_content
def get_token_age(self):
"""Return age of token"""
token_stat = os.stat(self.token_path)
token_modified = token_stat.st_mtime
return time.time() - token_modified
def make_request(self, url):
"""Send a cookie authenticated HTTP request to GOG"""
request = Request(url, cookies=self.load_cookies())
request.get()
if request.content.startswith(b"<"):
raise AuthenticationError("Token expired, please log in again")
return request.json
def make_api_request(self, url):
"""Send a token authenticated request to GOG"""
try:
token = self.load_token()
except AuthenticationError:
return
if self.get_token_age() > 2600:
self.request_token(refresh_token=token["refresh_token"])
token = self.load_token()
if not token:
logger.warning(
"Request to %s cancelled because the GOG token could not be acquired",
url,
)
return
headers = {"Authorization": "Bearer " + token["access_token"]}
request = Request(url, headers=headers, cookies=self.load_cookies())
try:
request.get()
except HTTPError:
logger.error(
"Failed to request %s, check your GOG credentials and internet connectivity",
url,
)
return
return request.json
def get_user_data(self):
"""Return GOG profile information"""
url = "https://embed.gog.com/userData.json"
return self.make_api_request(url)
def get_library(self):
"""Return the user's library of GOG games"""
if system.path_exists(self.cache_path):
logger.debug("Returning cached GOG library")
with open(self.cache_path, "r", encoding='utf-8') as gog_cache:
return json.load(gog_cache)
total_pages = 1
games = []
page = 1
while page <= total_pages:
products_response = self.get_products_page(page=page)
page += 1
total_pages = products_response["totalPages"]
games += products_response["products"]
with open(self.cache_path, "w", encoding='utf-8') as gog_cache:
json.dump(games, gog_cache)
return games
def get_service_game(self, gog_game):
return GOGGame.new_from_gog_game(gog_game)
def get_products_page(self, page=1, search=None):
"""Return a single page of games"""
if not self.is_authenticated():
raise AuthenticationError("User is not logged in")
params = {"mediaType": "1"}
if page:
params["page"] = page
if search:
params["search"] = search
url = self.embed_url + "/account/getFilteredProducts?" + urlencode(params)
return self.make_request(url)
def get_game_dlcs(self, product_id):
"""Return the list of DLC products the user owns for a game"""
game_details = self.get_game_details(product_id)
if not game_details["dlcs"]:
return []
all_products_url = game_details["dlcs"]["expanded_all_products_url"]
return self.make_api_request(all_products_url)
def get_game_details(self, product_id):
"""Return game information for a given game"""
if not product_id:
raise ValueError("Missing product ID")
logger.info("Getting game details for %s", product_id)
url = "{}/products/{}?expand=downloads&locale={}".format(self.api_url, product_id, self.locale)
return self.make_api_request(url)
def get_download_info(self, downlink):
"""Return file download information"""
logger.info("Getting download info for %s", downlink)
try:
response = self.make_api_request(downlink)
except HTTPError as ex:
logger.error("HTTP error: %s", ex)
raise UnavailableGame from ex
if not response:
raise UnavailableGame
for field in ("checksum", "downlink"):
field_url = response[field]
parsed = urlparse(field_url)
query = dict(parse_qsl(parsed.query))
response[field + "_filename"] = os.path.basename(query.get("path") or parsed.path)
return response
def get_downloads(self, gogid):
"""Return all available downloads for a GOG ID"""
if not gogid:
logger.warning("Unable to get GOG data because no GOG ID is available")
return {}
gog_data = self.get_game_details(gogid)
if not gog_data:
logger.warning("Unable to get GOG data for game %s", gogid)
return {}
return gog_data["downloads"]
def get_extras(self, gogid):
"""Return a list of bonus content available for a GOG ID and its DLCs"""
logger.debug("Download extras for GOG ID %s and its DLCs", gogid)
game = self.get_game_details(gogid)
if not game:
logger.warning("Unable to get GOG data for game %s", gogid)
return []
dlcs = self.get_game_dlcs(gogid)
products = [game, *dlcs] if dlcs else [game]
all_extras = {}
for product in products:
extras = [
{
"name": download.get("name", "").strip().capitalize(),
"type": download.get("type", "").strip(),
"total_size": download.get("total_size", 0),
"id": str(download["id"]),
} for download in product["downloads"].get("bonus_content") or []
]
if extras:
all_extras[product.get("title", "").strip()] = extras
return all_extras
def get_installers(self, downloads, runner, language="en"):
"""Return available installers for a GOG game"""
# Filter out Mac installers
gog_installers = [installer for installer in downloads.get("installers", []) if installer["os"] != "mac"]
available_platforms = {installer["os"] for installer in gog_installers}
# If it's a Linux game, also filter out Windows games
if "linux" in available_platforms:
filter_os = "windows" if runner == "linux" else "linux"
gog_installers = [installer for installer in gog_installers if installer["os"] != filter_os]
return [
installer
for installer in gog_installers
if installer["language"] == self.determine_language_installer(gog_installers, language)
]
def get_update_versions(self, gog_id):
"""Return updates available for a game, keyed by patch version"""
games_detail = self.get_game_details(gog_id)
patches = games_detail["downloads"]["patches"]
if not patches:
logger.info("No patches for %s", games_detail)
return {}
patch_versions = defaultdict(list)
for patch in patches:
patch_versions[patch["name"]].append(patch)
return patch_versions
def determine_language_installer(self, gog_installers, default_language="en"):
"""Return locale language string if available in gog_installers"""
language = i18n.get_lang()
gog_installers = [installer for installer in gog_installers if installer["language"] == language]
if not gog_installers:
language = default_language
return language
def query_download_links(self, download):
"""Convert files from the GOG API to a format compatible with lutris installers"""
download_links = []
for game_file in download.get("files", []):
downlink = game_file.get("downlink")
if not downlink:
logger.error("No download information for %s", game_file)
continue
download_info = self.get_download_info(downlink)
for field in ('checksum', 'downlink'):
download_links.append({
"name": download.get("name", ""),
"os": download.get("os", ""),
"type": download.get("type", ""),
"total_size": download.get("total_size", 0),
"id": str(game_file["id"]),
"url": download_info[field],
"filename": download_info[field + "_filename"]
})
return download_links
def get_extra_files(self, downloads, installer):
extra_files = []
for extra in downloads["bonus_content"]:
if str(extra["id"]) not in self.selected_extras:
continue
links = self.query_download_links(extra)
for link in links:
if link["filename"].endswith(".xml"):
# GOG gives a link for checksum XML files for bonus content
# but downloading them results in a 404 error.
continue
extra_files.append(
InstallerFile(installer.game_slug, str(extra["id"]), {
"url": link["url"],
"filename": link["filename"],
})
)
return extra_files
def _get_installer_links(self, installer, downloads):
"""Return links to downloadable files from a list of downloads"""
try:
gog_installers = self.get_installers(downloads, installer.runner)
if not gog_installers:
return []
if len(gog_installers) > 1:
logger.warning("More than 1 GOG installer found, picking first.")
_installer = gog_installers[0]
return self.query_download_links(_installer)
except HTTPError as err:
raise UnavailableGame("Couldn't load the download links for this game") from err
def get_patch_files(self, installer, installer_file_id):
logger.debug("Getting patches for %s", installer.version)
downloads = self.get_downloads(installer.service_appid)
links = []
for patch_file in downloads["patches"]:
if "GOG " + patch_file["version"] == installer.version:
links += self.query_download_links(patch_file)
return self._format_links(installer, installer_file_id, links)
def _format_links(self, installer, installer_file_id, links):
_installer_files = defaultdict(dict) # keyed by filename
for link in links:
try:
filename = link["filename"]
except KeyError:
logger.error("Invalid link: %s", link)
raise
if filename.lower().endswith(".xml"):
if filename != installer_file_id:
filename = filename[:-4]
_installer_files[filename]["checksum_url"] = link["url"]
continue
_installer_files[filename]["id"] = link["id"]
_installer_files[filename]["url"] = link["url"]
_installer_files[filename]["filename"] = filename
_installer_files[filename]["total_size"] = link["total_size"]
files = []
file_id_provided = False # Only assign installer_file_id once
for _file_id in _installer_files:
installer_file = _installer_files[_file_id]
if "url" not in installer_file:
raise ValueError("Invalid installer file %s" % installer_file)
filename = installer_file["filename"]
if filename.lower().endswith((".exe", ".sh")) and not file_id_provided:
file_id = installer_file_id
file_id_provided = True
else:
file_id = _file_id
files.append(InstallerFile(installer.game_slug, file_id, {
"url": installer_file["url"],
"filename": installer_file["filename"],
"checksum_url": installer_file.get("checksum_url")
}))
if not file_id_provided:
raise UnavailableGame("Unable to determine correct file to launch installer")
return files
def get_installer_files(self, installer, installer_file_id):
try:
downloads = self.get_downloads(installer.service_appid)
except HTTPError as err:
raise UnavailableGame("Couldn't load the downloads for this game") from err
links = self._get_installer_links(installer, downloads)
if links:
files = self._format_links(installer, installer_file_id, links)
else:
files = []
if self.selected_extras:
for extra_file in self.get_extra_files(downloads, installer):
files.append(extra_file)
return files
def read_file_checksum(self, file_path):
"""Return the MD5 checksum for a GOG file
Requires a GOG XML file as input
This has yet to be used.
"""
if not file_path.endswith(".xml"):
raise ValueError("Pass a XML file to return the checksum")
with open(file_path, encoding='utf-8') as checksum_file:
checksum_content = checksum_file.read()
root_elem = etree.fromstring(checksum_content)
return (root_elem.attrib["name"], root_elem.attrib["md5"])
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
platforms = [platform.lower() for platform, is_supported in details["worksOn"].items() if is_supported]
system_config = {}
if "linux" in platforms:
runner = "linux"
game_config = {"exe": AUTO_ELF_EXE}
script = [
{"extract": {"file": "goginstaller", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR"}},
]
else:
runner = "wine"
game_config = {"exe": AUTO_WIN32_EXE}
script = [
{"autosetup_gog_game": "goginstaller"},
]
return {
"name": db_game["name"],
"version": "GOG",
"slug": details["slug"],
"game_slug": slugify(db_game["name"]),
"runner": runner,
"gogid": db_game["appid"],
"script": {
"game": game_config,
"system": system_config,
"files": [
{"goginstaller": "N/A:Select the installer from GOG"}
],
"installer": script
}
}
def get_dlc_installers(self, db_game):
appid = db_game["service_id"]
dlcs = self.get_game_dlcs(appid)
installers = []
for dlc in dlcs:
dlc_id = "gogdlc-%s" % dlc["slug"]
installer = {
"name": db_game["name"],
"version": dlc["title"],
"slug": dlc["slug"],
"description": "DLC for %s" % db_game["name"],
"game_slug": slugify(db_game["name"]),
"runner": "wine",
"is_dlc": True,
"dlcid": dlc["id"],
"gogid": dlc["id"],
"script": {
"extends": db_game["installer_slug"],
"files": [
{dlc_id: "N/A:Select the patch from GOG"}
],
"installer": [
{"task": {"name": "wineexec", "executable": dlc_id}}
]
}
}
installers.append(installer)
return installers
def get_update_installers(self, db_game):
appid = db_game["service_id"]
patch_versions = self.get_update_versions(appid)
patch_installers = []
for version in patch_versions:
patch = patch_versions[version]
size = human_size(sum([part["total_size"] for part in patch]))
patch_id = "gogpatch-%s" % slugify(patch[0]["version"])
installer = {
"name": db_game["name"],
"description": patch[0]["name"] + " " + size,
"slug": db_game["installer_slug"],
"game_slug": db_game["slug"],
"version": "GOG " + patch[0]["version"],
"runner": "wine",
"script": {
"extends": db_game["installer_slug"],
"files": [
{patch_id: "N/A:Select the patch from GOG"}
],
"installer": [
{"task": {"name": "wineexec", "executable": patch_id}}
]
}
}
patch_installers.append(installer)
return patch_installers
api_url
¶
cache_path
¶
client_id
¶
client_secret
¶
cookies_path
¶
credential_files
property
readonly
¶
Return a list of all files used for authentication
default_format
¶
drm_free
¶
embed_url
¶
has_extras
¶
icon
¶
id
¶
is_loading
¶
login_success_url
¶
login_url
property
readonly
¶
Return authentication URL
medias
¶
name
¶
redirect_uri
¶
token_path
¶
__init__(self)
special
¶
Source code in lutris/services/gog.py
def __init__(self):
super().__init__()
self.selected_extras = None
gog_locales = {
"en": "en-US",
"de": "de-DE",
"fr": "fr-FR",
"pl": "pl-PL",
"ru": "ru-RU",
"zh": "zh-Hans",
}
self.locale = gog_locales.get(i18n.get_lang(), "en-US")
determine_language_installer(self, gog_installers, default_language='en')
¶
Return locale language string if available in gog_installers
Source code in lutris/services/gog.py
def determine_language_installer(self, gog_installers, default_language="en"):
"""Return locale language string if available in gog_installers"""
language = i18n.get_lang()
gog_installers = [installer for installer in gog_installers if installer["language"] == language]
if not gog_installers:
language = default_language
return language
generate_installer(self, db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/gog.py
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
platforms = [platform.lower() for platform, is_supported in details["worksOn"].items() if is_supported]
system_config = {}
if "linux" in platforms:
runner = "linux"
game_config = {"exe": AUTO_ELF_EXE}
script = [
{"extract": {"file": "goginstaller", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR"}},
]
else:
runner = "wine"
game_config = {"exe": AUTO_WIN32_EXE}
script = [
{"autosetup_gog_game": "goginstaller"},
]
return {
"name": db_game["name"],
"version": "GOG",
"slug": details["slug"],
"game_slug": slugify(db_game["name"]),
"runner": runner,
"gogid": db_game["appid"],
"script": {
"game": game_config,
"system": system_config,
"files": [
{"goginstaller": "N/A:Select the installer from GOG"}
],
"installer": script
}
}
get_dlc_installers(self, db_game)
¶
Source code in lutris/services/gog.py
def get_dlc_installers(self, db_game):
appid = db_game["service_id"]
dlcs = self.get_game_dlcs(appid)
installers = []
for dlc in dlcs:
dlc_id = "gogdlc-%s" % dlc["slug"]
installer = {
"name": db_game["name"],
"version": dlc["title"],
"slug": dlc["slug"],
"description": "DLC for %s" % db_game["name"],
"game_slug": slugify(db_game["name"]),
"runner": "wine",
"is_dlc": True,
"dlcid": dlc["id"],
"gogid": dlc["id"],
"script": {
"extends": db_game["installer_slug"],
"files": [
{dlc_id: "N/A:Select the patch from GOG"}
],
"installer": [
{"task": {"name": "wineexec", "executable": dlc_id}}
]
}
}
installers.append(installer)
return installers
get_download_info(self, downlink)
¶
Return file download information
Source code in lutris/services/gog.py
def get_download_info(self, downlink):
"""Return file download information"""
logger.info("Getting download info for %s", downlink)
try:
response = self.make_api_request(downlink)
except HTTPError as ex:
logger.error("HTTP error: %s", ex)
raise UnavailableGame from ex
if not response:
raise UnavailableGame
for field in ("checksum", "downlink"):
field_url = response[field]
parsed = urlparse(field_url)
query = dict(parse_qsl(parsed.query))
response[field + "_filename"] = os.path.basename(query.get("path") or parsed.path)
return response
get_downloads(self, gogid)
¶
Return all available downloads for a GOG ID
Source code in lutris/services/gog.py
def get_downloads(self, gogid):
"""Return all available downloads for a GOG ID"""
if not gogid:
logger.warning("Unable to get GOG data because no GOG ID is available")
return {}
gog_data = self.get_game_details(gogid)
if not gog_data:
logger.warning("Unable to get GOG data for game %s", gogid)
return {}
return gog_data["downloads"]
get_extra_files(self, downloads, installer)
¶
Source code in lutris/services/gog.py
def get_extra_files(self, downloads, installer):
extra_files = []
for extra in downloads["bonus_content"]:
if str(extra["id"]) not in self.selected_extras:
continue
links = self.query_download_links(extra)
for link in links:
if link["filename"].endswith(".xml"):
# GOG gives a link for checksum XML files for bonus content
# but downloading them results in a 404 error.
continue
extra_files.append(
InstallerFile(installer.game_slug, str(extra["id"]), {
"url": link["url"],
"filename": link["filename"],
})
)
return extra_files
get_extras(self, gogid)
¶
Return a list of bonus content available for a GOG ID and its DLCs
Source code in lutris/services/gog.py
def get_extras(self, gogid):
"""Return a list of bonus content available for a GOG ID and its DLCs"""
logger.debug("Download extras for GOG ID %s and its DLCs", gogid)
game = self.get_game_details(gogid)
if not game:
logger.warning("Unable to get GOG data for game %s", gogid)
return []
dlcs = self.get_game_dlcs(gogid)
products = [game, *dlcs] if dlcs else [game]
all_extras = {}
for product in products:
extras = [
{
"name": download.get("name", "").strip().capitalize(),
"type": download.get("type", "").strip(),
"total_size": download.get("total_size", 0),
"id": str(download["id"]),
} for download in product["downloads"].get("bonus_content") or []
]
if extras:
all_extras[product.get("title", "").strip()] = extras
return all_extras
get_game_details(self, product_id)
¶
Return game information for a given game
Source code in lutris/services/gog.py
def get_game_details(self, product_id):
"""Return game information for a given game"""
if not product_id:
raise ValueError("Missing product ID")
logger.info("Getting game details for %s", product_id)
url = "{}/products/{}?expand=downloads&locale={}".format(self.api_url, product_id, self.locale)
return self.make_api_request(url)
get_game_dlcs(self, product_id)
¶
Return the list of DLC products the user owns for a game
Source code in lutris/services/gog.py
def get_game_dlcs(self, product_id):
"""Return the list of DLC products the user owns for a game"""
game_details = self.get_game_details(product_id)
if not game_details["dlcs"]:
return []
all_products_url = game_details["dlcs"]["expanded_all_products_url"]
return self.make_api_request(all_products_url)
get_installer_files(self, installer, installer_file_id)
¶
Source code in lutris/services/gog.py
def get_installer_files(self, installer, installer_file_id):
try:
downloads = self.get_downloads(installer.service_appid)
except HTTPError as err:
raise UnavailableGame("Couldn't load the downloads for this game") from err
links = self._get_installer_links(installer, downloads)
if links:
files = self._format_links(installer, installer_file_id, links)
else:
files = []
if self.selected_extras:
for extra_file in self.get_extra_files(downloads, installer):
files.append(extra_file)
return files
get_installers(self, downloads, runner, language='en')
¶
Return available installers for a GOG game
Source code in lutris/services/gog.py
def get_installers(self, downloads, runner, language="en"):
"""Return available installers for a GOG game"""
# Filter out Mac installers
gog_installers = [installer for installer in downloads.get("installers", []) if installer["os"] != "mac"]
available_platforms = {installer["os"] for installer in gog_installers}
# If it's a Linux game, also filter out Windows games
if "linux" in available_platforms:
filter_os = "windows" if runner == "linux" else "linux"
gog_installers = [installer for installer in gog_installers if installer["os"] != filter_os]
return [
installer
for installer in gog_installers
if installer["language"] == self.determine_language_installer(gog_installers, language)
]
get_library(self)
¶
Return the user's library of GOG games
Source code in lutris/services/gog.py
def get_library(self):
"""Return the user's library of GOG games"""
if system.path_exists(self.cache_path):
logger.debug("Returning cached GOG library")
with open(self.cache_path, "r", encoding='utf-8') as gog_cache:
return json.load(gog_cache)
total_pages = 1
games = []
page = 1
while page <= total_pages:
products_response = self.get_products_page(page=page)
page += 1
total_pages = products_response["totalPages"]
games += products_response["products"]
with open(self.cache_path, "w", encoding='utf-8') as gog_cache:
json.dump(games, gog_cache)
return games
get_patch_files(self, installer, installer_file_id)
¶
Source code in lutris/services/gog.py
def get_patch_files(self, installer, installer_file_id):
logger.debug("Getting patches for %s", installer.version)
downloads = self.get_downloads(installer.service_appid)
links = []
for patch_file in downloads["patches"]:
if "GOG " + patch_file["version"] == installer.version:
links += self.query_download_links(patch_file)
return self._format_links(installer, installer_file_id, links)
get_products_page(self, page=1, search=None)
¶
Return a single page of games
Source code in lutris/services/gog.py
def get_products_page(self, page=1, search=None):
"""Return a single page of games"""
if not self.is_authenticated():
raise AuthenticationError("User is not logged in")
params = {"mediaType": "1"}
if page:
params["page"] = page
if search:
params["search"] = search
url = self.embed_url + "/account/getFilteredProducts?" + urlencode(params)
return self.make_request(url)
get_service_game(self, gog_game)
¶
Source code in lutris/services/gog.py
def get_service_game(self, gog_game):
return GOGGame.new_from_gog_game(gog_game)
get_token_age(self)
¶
Return age of token
Source code in lutris/services/gog.py
def get_token_age(self):
"""Return age of token"""
token_stat = os.stat(self.token_path)
token_modified = token_stat.st_mtime
return time.time() - token_modified
get_update_installers(self, db_game)
¶
Source code in lutris/services/gog.py
def get_update_installers(self, db_game):
appid = db_game["service_id"]
patch_versions = self.get_update_versions(appid)
patch_installers = []
for version in patch_versions:
patch = patch_versions[version]
size = human_size(sum([part["total_size"] for part in patch]))
patch_id = "gogpatch-%s" % slugify(patch[0]["version"])
installer = {
"name": db_game["name"],
"description": patch[0]["name"] + " " + size,
"slug": db_game["installer_slug"],
"game_slug": db_game["slug"],
"version": "GOG " + patch[0]["version"],
"runner": "wine",
"script": {
"extends": db_game["installer_slug"],
"files": [
{patch_id: "N/A:Select the patch from GOG"}
],
"installer": [
{"task": {"name": "wineexec", "executable": patch_id}}
]
}
}
patch_installers.append(installer)
return patch_installers
get_update_versions(self, gog_id)
¶
Return updates available for a game, keyed by patch version
Source code in lutris/services/gog.py
def get_update_versions(self, gog_id):
"""Return updates available for a game, keyed by patch version"""
games_detail = self.get_game_details(gog_id)
patches = games_detail["downloads"]["patches"]
if not patches:
logger.info("No patches for %s", games_detail)
return {}
patch_versions = defaultdict(list)
for patch in patches:
patch_versions[patch["name"]].append(patch)
return patch_versions
get_user_data(self)
¶
Return GOG profile information
Source code in lutris/services/gog.py
def get_user_data(self):
"""Return GOG profile information"""
url = "https://embed.gog.com/userData.json"
return self.make_api_request(url)
is_connected(self)
¶
Return whether the user is authenticated and if the service is available
Source code in lutris/services/gog.py
def is_connected(self):
"""Return whether the user is authenticated and if the service is available"""
if not self.is_authenticated():
return False
try:
user_data = self.get_user_data()
except UnauthorizedAccess:
logger.warning("GOG token is invalid")
return False
return user_data and "username" in user_data
load(self)
¶
Load the user game library from the GOG API
Source code in lutris/services/gog.py
def load(self):
"""Load the user game library from the GOG API"""
if self.is_loading:
logger.warning("GOG games are already loading")
return
if not self.is_connected():
logger.error("User not connected to GOG")
return
self.is_loading = True
games = [GOGGame.new_from_gog_game(game) for game in self.get_library()]
for game in games:
game.save()
self.match_games()
self.is_loading = False
return games
load_token(self)
¶
Load token from disk
Source code in lutris/services/gog.py
def load_token(self):
"""Load token from disk"""
if not os.path.exists(self.token_path):
raise AuthenticationError("No GOG token available")
with open(self.token_path, encoding='utf-8') as token_file:
token_content = json.loads(token_file.read())
return token_content
login_callback(self, url)
¶
Source code in lutris/services/gog.py
def login_callback(self, url):
return self.request_token(url)
make_api_request(self, url)
¶
Send a token authenticated request to GOG
Source code in lutris/services/gog.py
def make_api_request(self, url):
"""Send a token authenticated request to GOG"""
try:
token = self.load_token()
except AuthenticationError:
return
if self.get_token_age() > 2600:
self.request_token(refresh_token=token["refresh_token"])
token = self.load_token()
if not token:
logger.warning(
"Request to %s cancelled because the GOG token could not be acquired",
url,
)
return
headers = {"Authorization": "Bearer " + token["access_token"]}
request = Request(url, headers=headers, cookies=self.load_cookies())
try:
request.get()
except HTTPError:
logger.error(
"Failed to request %s, check your GOG credentials and internet connectivity",
url,
)
return
return request.json
make_request(self, url)
¶
Send a cookie authenticated HTTP request to GOG
Source code in lutris/services/gog.py
def make_request(self, url):
"""Send a cookie authenticated HTTP request to GOG"""
request = Request(url, cookies=self.load_cookies())
request.get()
if request.content.startswith(b"<"):
raise AuthenticationError("Token expired, please log in again")
return request.json
query_download_links(self, download)
¶
Convert files from the GOG API to a format compatible with lutris installers
Source code in lutris/services/gog.py
def query_download_links(self, download):
"""Convert files from the GOG API to a format compatible with lutris installers"""
download_links = []
for game_file in download.get("files", []):
downlink = game_file.get("downlink")
if not downlink:
logger.error("No download information for %s", game_file)
continue
download_info = self.get_download_info(downlink)
for field in ('checksum', 'downlink'):
download_links.append({
"name": download.get("name", ""),
"os": download.get("os", ""),
"type": download.get("type", ""),
"total_size": download.get("total_size", 0),
"id": str(game_file["id"]),
"url": download_info[field],
"filename": download_info[field + "_filename"]
})
return download_links
read_file_checksum(self, file_path)
¶
Return the MD5 checksum for a GOG file Requires a GOG XML file as input This has yet to be used.
Source code in lutris/services/gog.py
def read_file_checksum(self, file_path):
"""Return the MD5 checksum for a GOG file
Requires a GOG XML file as input
This has yet to be used.
"""
if not file_path.endswith(".xml"):
raise ValueError("Pass a XML file to return the checksum")
with open(file_path, encoding='utf-8') as checksum_file:
checksum_content = checksum_file.read()
root_elem = etree.fromstring(checksum_content)
return (root_elem.attrib["name"], root_elem.attrib["md5"])
request_token(self, url='', refresh_token='')
¶
Get authentication token from GOG
Source code in lutris/services/gog.py
def request_token(self, url="", refresh_token=""):
"""Get authentication token from GOG"""
if refresh_token:
grant_type = "refresh_token"
extra_params = {"refresh_token": refresh_token}
else:
grant_type = "authorization_code"
parsed_url = urlparse(url)
response_params = dict(parse_qsl(parsed_url.query))
if "code" not in response_params:
logger.error("code not received from GOG")
logger.error(response_params)
return
extra_params = {
"code": response_params["code"],
"redirect_uri": self.redirect_uri,
}
params = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": grant_type,
}
params.update(extra_params)
url = "https://auth.gog.com/token?" + urlencode(params)
request = Request(url)
try:
request.get()
except HTTPError:
logger.error("Failed to get token, check your GOG credentials.")
logger.warning("Clearing existing credentials")
self.logout()
return
token = request.json
with open(self.token_path, "w", encoding='utf-8') as token_file:
token_file.write(json.dumps(token))
if not refresh_token:
self.emit("service-login")
GogLargeBanner (GogSmallBanner)
¶
GogMediumBanner (GogSmallBanner)
¶
GogSmallBanner (ServiceMedia)
¶
Small size game logo
Source code in lutris/services/gog.py
class GogSmallBanner(ServiceMedia):
"""Small size game logo"""
service = "gog"
size = (100, 60)
dest_path = os.path.join(settings.CACHE_DIR, "gog/banners/small")
file_pattern = "%s.jpg"
api_field = "image"
url_pattern = "https:%s_prof_game_100x60.jpg"
humblebundle
¶
Manage Humble Bundle libraries
HumbleBigIcon (HumbleBundleIcon)
¶
Source code in lutris/services/humblebundle.py
class HumbleBigIcon(HumbleBundleIcon):
size = (105, 105)
size
¶
HumbleBundleGame (ServiceGame)
¶
Service game for DRM free Humble Bundle games
Source code in lutris/services/humblebundle.py
class HumbleBundleGame(ServiceGame):
"""Service game for DRM free Humble Bundle games"""
service = "humblebundle"
@classmethod
def new_from_humble_game(cls, humble_game):
"""Converts a game from the API to a service game usable by Lutris"""
service_game = HumbleBundleGame()
service_game.appid = humble_game["machine_name"]
service_game.slug = humble_game["machine_name"]
service_game.name = humble_game["human_name"]
service_game.details = json.dumps(humble_game)
return service_game
service
¶
new_from_humble_game(humble_game)
classmethod
¶
Converts a game from the API to a service game usable by Lutris
Source code in lutris/services/humblebundle.py
@classmethod
def new_from_humble_game(cls, humble_game):
"""Converts a game from the API to a service game usable by Lutris"""
service_game = HumbleBundleGame()
service_game.appid = humble_game["machine_name"]
service_game.slug = humble_game["machine_name"]
service_game.name = humble_game["human_name"]
service_game.details = json.dumps(humble_game)
return service_game
HumbleBundleIcon (ServiceMedia)
¶
HumbleBundle icon
Source code in lutris/services/humblebundle.py
class HumbleBundleIcon(ServiceMedia):
"""HumbleBundle icon"""
service = "humblebundle"
size = (70, 70)
dest_path = os.path.join(settings.CACHE_DIR, "humblebundle/icons")
file_pattern = "%s.png"
api_field = "icon"
HumbleBundleService (OnlineService)
¶
Service for Humble Bundle
Source code in lutris/services/humblebundle.py
class HumbleBundleService(OnlineService):
"""Service for Humble Bundle"""
id = "humblebundle"
_matcher = "humble"
name = _("Humble Bundle")
icon = "humblebundle"
online = True
drm_free = True
medias = {
"small_icon": HumbleSmallIcon,
"icon": HumbleBundleIcon,
"big_icon": HumbleBigIcon
}
default_format = "icon"
api_url = "https://www.humblebundle.com/"
login_url = "https://www.humblebundle.com/login?goto=/home/library"
redirect_uri = "https://www.humblebundle.com/home/library"
cookies_path = os.path.join(settings.CACHE_DIR, ".humblebundle.auth")
token_path = os.path.join(settings.CACHE_DIR, ".humblebundle.token")
cache_path = os.path.join(settings.CACHE_DIR, "humblebundle/library/")
supported_platforms = ("linux", "windows")
is_loading = False
def login_callback(self, url):
"""Called after the user has logged in successfully"""
self.emit("service-login")
def is_connected(self):
"""This doesn't actually check if the authentication
is valid like the GOG service does.
"""
return self.is_authenticated()
def load(self):
"""Load the user's Humble Bundle library"""
if self.is_loading:
logger.warning("Humble bundle games are already loading")
return
self.is_loading = True
try:
library = self.get_library()
except ValueError:
logger.error("Failed to get Humble Bundle library. Try logging out and back-in.")
return
humble_games = []
seen = set()
for game in library:
if game["human_name"] in seen:
continue
humble_games.append(HumbleBundleGame.new_from_humble_game(game))
seen.add(game["human_name"])
for game in humble_games:
game.save()
self.is_loading = False
return humble_games
def make_api_request(self, url):
"""Make an authenticated request to the Humble API"""
request = Request(url, cookies=self.load_cookies())
try:
request.get()
except HTTPError:
logger.error(
"Failed to request %s, check your Humble Bundle credentials and internet connectivity",
url,
)
return
return request.json
def order_path(self, gamekey):
"""Return the local path for an order"""
return os.path.join(self.cache_path, "%s.json" % gamekey)
def get_order(self, gamekey):
"""Retrieve an order identitied by its key"""
# logger.debug("Getting Humble Bundle order %s", gamekey)
cache_filename = self.order_path(gamekey)
if os.path.exists(cache_filename):
with open(cache_filename, encoding='utf-8') as cache_file:
return json.load(cache_file)
response = self.make_api_request(self.api_url + "api/v1/order/%s?all_tpkds=true" % gamekey)
os.makedirs(self.cache_path, exist_ok=True)
with open(cache_filename, "w", encoding='utf-8') as cache_file:
json.dump(response, cache_file)
return response
def get_library(self):
"""Return the games from the user's library"""
games = []
for order in self.get_orders():
if not order:
continue
for product in order["subproducts"]:
for download in product["downloads"]:
if download["platform"] in self.supported_platforms:
games.append(product)
return games
def get_gamekeys_from_local_orders(self):
"""Retrieve a list of orders from the cache."""
game_keys = []
if os.path.exists(self.cache_path):
for order_file in os.listdir(self.cache_path):
if not order_file.endswith(".json"):
continue
game_keys.append({"gamekey": order_file[:-5]})
return game_keys
def get_orders(self):
"""Return all orders"""
gamekeys = self.get_gamekeys_from_local_orders()
orders = []
if not gamekeys:
gamekeys = self.make_api_request(self.api_url + "api/v1/user/order")
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
future_orders = [
executor.submit(self.get_order, gamekey["gamekey"])
for gamekey in gamekeys
]
for order in future_orders:
orders.append(order.result())
logger.info("Loaded %s Humble Bundle orders", len(orders))
return orders
@staticmethod
def find_download_in_order(order, humbleid, platform):
"""Return the download information in an order for a give game"""
for product in order["subproducts"]:
if product["machine_name"] != humbleid:
continue
available_platforms = [d["platform"] for d in product["downloads"]]
if platform not in available_platforms:
logger.warning("Requested platform %s not available in available platforms: %s",
platform, available_platforms)
if "linux" in available_platforms:
platform = "linux"
elif "windows" in available_platforms:
platform = "windows"
else:
platform = available_platforms[0]
for download in product["downloads"]:
if download["platform"] != platform:
continue
return {
"product": order["product"],
"gamekey": order["gamekey"],
"created": order["created"],
"download": download
}
def get_downloads(self, humbleid, platform):
"""Return the download information for a given game"""
download_links = []
for order in self.get_orders():
download = self.find_download_in_order(order, humbleid, platform)
if download:
download_links.append(download)
return download_links
def get_installer_files(self, installer, installer_file_id):
"""Replace the user provided file with download links from Humble Bundle"""
try:
link = get_humble_download_link(installer.service_appid, installer.runner)
except Exception as ex:
logger.exception("Failed to get Humble Bundle game: %s", ex)
raise UnavailableGame from ex
if not link:
raise UnavailableGame("No game found on Humble Bundle")
filename = link.split("?")[0].split("/")[-1]
return [
InstallerFile(installer.game_slug, installer_file_id, {
"url": link,
"filename": filename
})
]
@staticmethod
def get_filename_for_platform(downloads, platform):
download = [d for d in downloads if d["platform"] == platform][0]
url = pick_download_url_from_download_info(download)
if not url:
return
return url.split("?")[0].split("/")[-1]
@staticmethod
def platform_has_downloads(downloads, platform):
for download in downloads:
if download["platform"] != platform:
continue
if len(download["download_struct"]) > 0:
return True
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
platforms = [download["platform"] for download in details["downloads"]]
system_config = {}
if "linux" in platforms and self.platform_has_downloads(details["downloads"], "linux"):
runner = "linux"
game_config = {"exe": AUTO_ELF_EXE}
filename = self.get_filename_for_platform(details["downloads"], "linux")
if filename.lower().endswith(".sh"):
script = [
{"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/noarch", "dst": "$CACHE/noarch", "optional": True}},
{"merge": {"src": "$CACHE/data/x86_64", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/x86_64", "dst": "$CACHE/x86_64", "optional": True}},
{"merge": {"src": "$CACHE/data/x86", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/x86", "dst": "$CACHE/x86", "optional": True}},
{"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR", "optional": True}},
]
elif filename.endswith("-bin") or filename.endswith("mojo.run"):
script = [
{"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR"}},
]
elif filename.endswith(".air"):
script = [
{"move": {"src": "humblegame", "dst": "$GAMEDIR"}},
]
else:
script = [{"extract": {"file": "humblegame"}}]
system_config = {"gamemode": 'false'} # Unity games crash with gamemode
elif "windows" in platforms:
runner = "wine"
game_config = {"exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"}
filename = self.get_filename_for_platform(details["downloads"], "windows")
if filename.lower().endswith(".zip"):
script = [
{"task": {"name": "create_prefix", "prefix": "$GAMEDIR"}},
{"extract": {"file": "humblegame", "dst": "$GAMEDIR/drive_c/%s" % db_game["slug"]}}
]
else:
script = [
{"task": {"name": "wineexec", "executable": "humblegame"}}
]
else:
logger.warning("Unsupported platforms: %s", platforms)
return {}
return {
"name": db_game["name"],
"version": "Humble Bundle",
"slug": details["machine_name"],
"game_slug": slugify(db_game["name"]),
"runner": runner,
"humbleid": db_game["appid"],
"script": {
"game": game_config,
"system": system_config,
"files": [
{"humblegame": "N/A:Select the installer from Humble Bundle"}
],
"installer": script
}
}
api_url
¶
cache_path
¶
cookies_path
¶
default_format
¶
drm_free
¶
icon
¶
id
¶
is_loading
¶
login_url
¶
medias
¶
name
¶
online
¶
redirect_uri
¶
supported_platforms
¶
token_path
¶
find_download_in_order(order, humbleid, platform)
staticmethod
¶
Return the download information in an order for a give game
Source code in lutris/services/humblebundle.py
@staticmethod
def find_download_in_order(order, humbleid, platform):
"""Return the download information in an order for a give game"""
for product in order["subproducts"]:
if product["machine_name"] != humbleid:
continue
available_platforms = [d["platform"] for d in product["downloads"]]
if platform not in available_platforms:
logger.warning("Requested platform %s not available in available platforms: %s",
platform, available_platforms)
if "linux" in available_platforms:
platform = "linux"
elif "windows" in available_platforms:
platform = "windows"
else:
platform = available_platforms[0]
for download in product["downloads"]:
if download["platform"] != platform:
continue
return {
"product": order["product"],
"gamekey": order["gamekey"],
"created": order["created"],
"download": download
}
generate_installer(self, db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/humblebundle.py
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
platforms = [download["platform"] for download in details["downloads"]]
system_config = {}
if "linux" in platforms and self.platform_has_downloads(details["downloads"], "linux"):
runner = "linux"
game_config = {"exe": AUTO_ELF_EXE}
filename = self.get_filename_for_platform(details["downloads"], "linux")
if filename.lower().endswith(".sh"):
script = [
{"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/noarch", "dst": "$CACHE/noarch", "optional": True}},
{"merge": {"src": "$CACHE/data/x86_64", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/x86_64", "dst": "$CACHE/x86_64", "optional": True}},
{"merge": {"src": "$CACHE/data/x86", "dst": "$GAMEDIR", "optional": True}},
{"move": {"src": "$CACHE/data/x86", "dst": "$CACHE/x86", "optional": True}},
{"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR", "optional": True}},
]
elif filename.endswith("-bin") or filename.endswith("mojo.run"):
script = [
{"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
{"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR"}},
]
elif filename.endswith(".air"):
script = [
{"move": {"src": "humblegame", "dst": "$GAMEDIR"}},
]
else:
script = [{"extract": {"file": "humblegame"}}]
system_config = {"gamemode": 'false'} # Unity games crash with gamemode
elif "windows" in platforms:
runner = "wine"
game_config = {"exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"}
filename = self.get_filename_for_platform(details["downloads"], "windows")
if filename.lower().endswith(".zip"):
script = [
{"task": {"name": "create_prefix", "prefix": "$GAMEDIR"}},
{"extract": {"file": "humblegame", "dst": "$GAMEDIR/drive_c/%s" % db_game["slug"]}}
]
else:
script = [
{"task": {"name": "wineexec", "executable": "humblegame"}}
]
else:
logger.warning("Unsupported platforms: %s", platforms)
return {}
return {
"name": db_game["name"],
"version": "Humble Bundle",
"slug": details["machine_name"],
"game_slug": slugify(db_game["name"]),
"runner": runner,
"humbleid": db_game["appid"],
"script": {
"game": game_config,
"system": system_config,
"files": [
{"humblegame": "N/A:Select the installer from Humble Bundle"}
],
"installer": script
}
}
get_downloads(self, humbleid, platform)
¶
Return the download information for a given game
Source code in lutris/services/humblebundle.py
def get_downloads(self, humbleid, platform):
"""Return the download information for a given game"""
download_links = []
for order in self.get_orders():
download = self.find_download_in_order(order, humbleid, platform)
if download:
download_links.append(download)
return download_links
get_filename_for_platform(downloads, platform)
staticmethod
¶
Source code in lutris/services/humblebundle.py
@staticmethod
def get_filename_for_platform(downloads, platform):
download = [d for d in downloads if d["platform"] == platform][0]
url = pick_download_url_from_download_info(download)
if not url:
return
return url.split("?")[0].split("/")[-1]
get_gamekeys_from_local_orders(self)
¶
Retrieve a list of orders from the cache.
Source code in lutris/services/humblebundle.py
def get_gamekeys_from_local_orders(self):
"""Retrieve a list of orders from the cache."""
game_keys = []
if os.path.exists(self.cache_path):
for order_file in os.listdir(self.cache_path):
if not order_file.endswith(".json"):
continue
game_keys.append({"gamekey": order_file[:-5]})
return game_keys
get_installer_files(self, installer, installer_file_id)
¶
Replace the user provided file with download links from Humble Bundle
Source code in lutris/services/humblebundle.py
def get_installer_files(self, installer, installer_file_id):
"""Replace the user provided file with download links from Humble Bundle"""
try:
link = get_humble_download_link(installer.service_appid, installer.runner)
except Exception as ex:
logger.exception("Failed to get Humble Bundle game: %s", ex)
raise UnavailableGame from ex
if not link:
raise UnavailableGame("No game found on Humble Bundle")
filename = link.split("?")[0].split("/")[-1]
return [
InstallerFile(installer.game_slug, installer_file_id, {
"url": link,
"filename": filename
})
]
get_library(self)
¶
Return the games from the user's library
Source code in lutris/services/humblebundle.py
def get_library(self):
"""Return the games from the user's library"""
games = []
for order in self.get_orders():
if not order:
continue
for product in order["subproducts"]:
for download in product["downloads"]:
if download["platform"] in self.supported_platforms:
games.append(product)
return games
get_order(self, gamekey)
¶
Retrieve an order identitied by its key
Source code in lutris/services/humblebundle.py
def get_order(self, gamekey):
"""Retrieve an order identitied by its key"""
# logger.debug("Getting Humble Bundle order %s", gamekey)
cache_filename = self.order_path(gamekey)
if os.path.exists(cache_filename):
with open(cache_filename, encoding='utf-8') as cache_file:
return json.load(cache_file)
response = self.make_api_request(self.api_url + "api/v1/order/%s?all_tpkds=true" % gamekey)
os.makedirs(self.cache_path, exist_ok=True)
with open(cache_filename, "w", encoding='utf-8') as cache_file:
json.dump(response, cache_file)
return response
get_orders(self)
¶
Return all orders
Source code in lutris/services/humblebundle.py
def get_orders(self):
"""Return all orders"""
gamekeys = self.get_gamekeys_from_local_orders()
orders = []
if not gamekeys:
gamekeys = self.make_api_request(self.api_url + "api/v1/user/order")
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
future_orders = [
executor.submit(self.get_order, gamekey["gamekey"])
for gamekey in gamekeys
]
for order in future_orders:
orders.append(order.result())
logger.info("Loaded %s Humble Bundle orders", len(orders))
return orders
is_connected(self)
¶
This doesn't actually check if the authentication is valid like the GOG service does.
Source code in lutris/services/humblebundle.py
def is_connected(self):
"""This doesn't actually check if the authentication
is valid like the GOG service does.
"""
return self.is_authenticated()
load(self)
¶
Load the user's Humble Bundle library
Source code in lutris/services/humblebundle.py
def load(self):
"""Load the user's Humble Bundle library"""
if self.is_loading:
logger.warning("Humble bundle games are already loading")
return
self.is_loading = True
try:
library = self.get_library()
except ValueError:
logger.error("Failed to get Humble Bundle library. Try logging out and back-in.")
return
humble_games = []
seen = set()
for game in library:
if game["human_name"] in seen:
continue
humble_games.append(HumbleBundleGame.new_from_humble_game(game))
seen.add(game["human_name"])
for game in humble_games:
game.save()
self.is_loading = False
return humble_games
login_callback(self, url)
¶
Called after the user has logged in successfully
Source code in lutris/services/humblebundle.py
def login_callback(self, url):
"""Called after the user has logged in successfully"""
self.emit("service-login")
make_api_request(self, url)
¶
Make an authenticated request to the Humble API
Source code in lutris/services/humblebundle.py
def make_api_request(self, url):
"""Make an authenticated request to the Humble API"""
request = Request(url, cookies=self.load_cookies())
try:
request.get()
except HTTPError:
logger.error(
"Failed to request %s, check your Humble Bundle credentials and internet connectivity",
url,
)
return
return request.json
order_path(self, gamekey)
¶
Return the local path for an order
Source code in lutris/services/humblebundle.py
def order_path(self, gamekey):
"""Return the local path for an order"""
return os.path.join(self.cache_path, "%s.json" % gamekey)
platform_has_downloads(downloads, platform)
staticmethod
¶
Source code in lutris/services/humblebundle.py
@staticmethod
def platform_has_downloads(downloads, platform):
for download in downloads:
if download["platform"] != platform:
continue
if len(download["download_struct"]) > 0:
return True
HumbleSmallIcon (HumbleBundleIcon)
¶
Source code in lutris/services/humblebundle.py
class HumbleSmallIcon(HumbleBundleIcon):
size = (35, 35)
size
¶
get_humble_download_link(humbleid, runner)
¶
Return a download link for a given humbleid and runner
Source code in lutris/services/humblebundle.py
def get_humble_download_link(humbleid, runner):
"""Return a download link for a given humbleid and runner"""
service = HumbleBundleService()
platform = runner if runner != "wine" else "windows"
downloads = service.get_downloads(humbleid, platform)
if not downloads:
logger.error("Game %s for %s not found in the Humble Bundle library", humbleid, platform)
return
logger.info("Found %s download for %s", len(downloads), humbleid)
download = downloads[0]
logger.info("Reloading order %s", download["product"]["human_name"])
os.remove(service.order_path(download["gamekey"]))
order = service.get_order(download["gamekey"])
download_info = service.find_download_in_order(order, humbleid, platform)
if download_info:
return pick_download_url_from_download_info(download_info["download"])
logger.warning("Couldn't retrieve any downloads for %s", humbleid)
pick_download_url_from_download_info(download_info)
¶
From a list of downloads in Humble Bundle, pick the most appropriate one for the installer. This needs a way to be explicitely filtered.
Source code in lutris/services/humblebundle.py
def pick_download_url_from_download_info(download_info):
"""From a list of downloads in Humble Bundle, pick the most appropriate one
for the installer.
This needs a way to be explicitely filtered.
"""
if not download_info["download_struct"]:
logger.warning("No downloads found")
return
def humble_sort(download):
name = download["name"]
if "rpm" in name:
return -99 # Not supported as an extractor
bonus = 1
if "deb" not in name:
bonus = 2
if linux.LINUX_SYSTEM.is_64_bit:
if "386" in name or "32" in name:
return -1
else:
if "64" in name:
return -10
return 1 * bonus
sorted_downloads = sorted(download_info["download_struct"], key=humble_sort, reverse=True)
logger.debug("Humble bundle installers:")
for download in sorted_downloads:
logger.debug(download)
return sorted_downloads[0]["url"]["web"]
itchio
¶
Itch.io service. Not ready yet.
ItchIoService (OnlineService)
¶
lutris
¶
LutrisGame (ServiceGame)
¶
Service game created from the Lutris API
Source code in lutris/services/lutris.py
class LutrisGame(ServiceGame):
"""Service game created from the Lutris API"""
service = "lutris"
@classmethod
def new_from_api(cls, api_payload):
"""Create an instance of LutrisGame from the API response"""
service_game = LutrisGame()
service_game.appid = api_payload['slug']
service_game.slug = api_payload['slug']
service_game.name = api_payload['name']
service_game.details = json.dumps(api_payload)
return service_game
service
¶
new_from_api(api_payload)
classmethod
¶
Create an instance of LutrisGame from the API response
Source code in lutris/services/lutris.py
@classmethod
def new_from_api(cls, api_payload):
"""Create an instance of LutrisGame from the API response"""
service_game = LutrisGame()
service_game.appid = api_payload['slug']
service_game.slug = api_payload['slug']
service_game.name = api_payload['name']
service_game.details = json.dumps(api_payload)
return service_game
LutrisService (OnlineService)
¶
Service for Lutris games
Source code in lutris/services/lutris.py
class LutrisService(OnlineService):
"""Service for Lutris games"""
id = "lutris"
name = _("Lutris")
icon = "lutris"
online = True
medias = {
"icon": LutrisIcon,
"banner": LutrisBanner,
"coverart_med": LutrisCoverartMedium,
"coverart_big": LutrisCoverart,
}
default_format = "banner"
api_url = settings.SITE_URL + "/api"
login_url = settings.SITE_URL + "/api/accounts/token"
cache_path = os.path.join(settings.CACHE_DIR, "lutris")
token_path = os.path.join(settings.CACHE_DIR, "auth-token")
is_loading = False
@property
def credential_files(self):
"""Return a list of all files used for authentication"""
return [self.token_path]
def match_games(self):
"""Matching lutris games is much simpler... No API call needed."""
service_games = {
str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
}
for lutris_game in get_games():
self.match_game(service_games.get(lutris_game["slug"]), lutris_game)
def is_connected(self):
"""Is the service connected?"""
return self.is_authenticated()
def login(self, parent=None):
"""Connect to Lutris"""
login_dialog = dialogs.ClientLoginDialog(parent=parent)
login_dialog.connect("connected", self.on_connect_success)
def on_connect_success(self, _widget, _username):
"""Handles connection success"""
self.emit("service-login")
def get_library(self):
"""Return the remote library as a list of dicts."""
credentials = read_api_key()
if not credentials:
return []
url = settings.SITE_URL + "/api/games/library/%s" % urllib.parse.quote(credentials["username"])
request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
try:
response = request.get()
except http.HTTPError as ex:
logger.error("Unable to load library: %s", ex)
return []
response_data = response.json
if response_data:
return response_data["games"]
return []
def load(self):
if self.is_loading:
logger.warning("Lutris games are already loading")
return
self.is_loading = True
lutris_games = self.get_library()
logger.debug("Loaded %s games from Lutris library", len(lutris_games))
for game in lutris_games:
lutris_game = LutrisGame.new_from_api(game)
lutris_game.save()
logger.debug("Matching with already installed games")
self.match_games()
self.is_loading = False
logger.debug("Lutris games loaded")
return lutris_games
def install(self, db_game):
if isinstance(db_game, dict):
slug = db_game["slug"]
else:
slug = db_game
installers = get_game_installers(slug)
if not installers:
logger.warning("No installer for %s", slug)
return
application = Gio.Application.get_default()
application.show_installer_window(installers)
api_url
¶
cache_path
¶
credential_files
property
readonly
¶
Return a list of all files used for authentication
default_format
¶
icon
¶
id
¶
is_loading
¶
login_url
¶
medias
¶
name
¶
online
¶
token_path
¶
get_library(self)
¶
Return the remote library as a list of dicts.
Source code in lutris/services/lutris.py
def get_library(self):
"""Return the remote library as a list of dicts."""
credentials = read_api_key()
if not credentials:
return []
url = settings.SITE_URL + "/api/games/library/%s" % urllib.parse.quote(credentials["username"])
request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
try:
response = request.get()
except http.HTTPError as ex:
logger.error("Unable to load library: %s", ex)
return []
response_data = response.json
if response_data:
return response_data["games"]
return []
install(self, db_game)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/lutris.py
def install(self, db_game):
if isinstance(db_game, dict):
slug = db_game["slug"]
else:
slug = db_game
installers = get_game_installers(slug)
if not installers:
logger.warning("No installer for %s", slug)
return
application = Gio.Application.get_default()
application.show_installer_window(installers)
is_connected(self)
¶
Is the service connected?
Source code in lutris/services/lutris.py
def is_connected(self):
"""Is the service connected?"""
return self.is_authenticated()
load(self)
¶
Source code in lutris/services/lutris.py
def load(self):
if self.is_loading:
logger.warning("Lutris games are already loading")
return
self.is_loading = True
lutris_games = self.get_library()
logger.debug("Loaded %s games from Lutris library", len(lutris_games))
for game in lutris_games:
lutris_game = LutrisGame.new_from_api(game)
lutris_game.save()
logger.debug("Matching with already installed games")
self.match_games()
self.is_loading = False
logger.debug("Lutris games loaded")
return lutris_games
login(self, parent=None)
¶
Connect to Lutris
Source code in lutris/services/lutris.py
def login(self, parent=None):
"""Connect to Lutris"""
login_dialog = dialogs.ClientLoginDialog(parent=parent)
login_dialog.connect("connected", self.on_connect_success)
match_games(self)
¶
Matching lutris games is much simpler... No API call needed.
Source code in lutris/services/lutris.py
def match_games(self):
"""Matching lutris games is much simpler... No API call needed."""
service_games = {
str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
}
for lutris_game in get_games():
self.match_game(service_games.get(lutris_game["slug"]), lutris_game)
on_connect_success(self, _widget, _username)
¶
Handles connection success
Source code in lutris/services/lutris.py
def on_connect_success(self, _widget, _username):
"""Handles connection success"""
self.emit("service-login")
download_lutris_media(slug)
¶
Download all media types for a single lutris game
Source code in lutris/services/lutris.py
def download_lutris_media(slug):
"""Download all media types for a single lutris game"""
url = settings.SITE_URL + "/api/games/%s" % slug
request = http.Request(url)
try:
response = request.get()
except http.HTTPError as ex:
logger.debug("Unable to load %s: %s", slug, ex)
return
response_data = response.json
icon_url = response_data.get("icon_url")
if icon_url:
download_media({slug: icon_url}, LutrisIcon())
banner_url = response_data.get("banner_url")
if banner_url:
download_media({slug: banner_url}, LutrisBanner())
cover_url = response_data.get("coverart")
if cover_url:
download_media({slug: cover_url}, LutrisCoverart())
sync_media()
¶
Downlad all missing media
Source code in lutris/services/lutris.py
def sync_media():
"""Downlad all missing media"""
banners_available = {fn.split(".")[0] for fn in os.listdir(settings.BANNER_PATH)}
icons_available = {
fn.split(".")[0].replace("lutris_", "")
for fn in os.listdir(settings.ICON_PATH)
if fn.startswith("lutris_")
}
covers_available = {fn.split(".")[0] for fn in os.listdir(settings.COVERART_PATH)}
complete_games = banners_available.intersection(icons_available).intersection(covers_available)
all_slugs = {game["slug"] for game in get_games()}
slugs = all_slugs - complete_games
if not slugs:
return
games = get_api_games(list(slugs))
alias_map = {}
api_slugs = set()
for game in games:
api_slugs.add(game["slug"])
for alias in game["aliases"]:
if alias["slug"] in slugs:
alias_map[game["slug"]] = alias["slug"]
alias_slugs = set(alias_map.values())
used_alias_slugs = alias_slugs - api_slugs
for alias_slug in used_alias_slugs:
for game in games:
if alias_slug in [alias["slug"] for alias in game["aliases"]]:
game["slug"] = alias_map[game["slug"]]
continue
banner_urls = {
game["slug"]: game["banner_url"]
for game in games
if game["slug"] not in banners_available and game["banner_url"]
}
icon_urls = {
game["slug"]: game["icon_url"]
for game in games
if game["slug"] not in icons_available and game["icon_url"]
}
cover_urls = {
game["slug"]: game["coverart"]
for game in games
if game["slug"] not in covers_available and game["coverart"]
}
logger.debug(
"Syncing %s banners, %s icons and %s covers",
len(banner_urls), len(icon_urls), len(cover_urls)
)
download_media(banner_urls, LutrisBanner())
download_media(icon_urls, LutrisIcon())
download_media(cover_urls, LutrisCoverart())
mame
¶
MAME service Not ready yet
MAMEService (BaseService)
¶
origin
¶
EA Origin service.
OriginGame (ServiceGame)
¶
Source code in lutris/services/origin.py
class OriginGame(ServiceGame):
service = "origin"
@classmethod
def new_from_api(cls, offer):
origin_game = OriginGame()
origin_game.appid = offer["offerId"]
origin_game.slug = offer["gameNameFacetKey"]
origin_game.name = offer["i18n"]["displayName"]
origin_game.details = json.dumps(offer)
return origin_game
service
¶
new_from_api(offer)
classmethod
¶
Source code in lutris/services/origin.py
@classmethod
def new_from_api(cls, offer):
origin_game = OriginGame()
origin_game.appid = offer["offerId"]
origin_game.slug = offer["gameNameFacetKey"]
origin_game.name = offer["i18n"]["displayName"]
origin_game.details = json.dumps(offer)
return origin_game
OriginLauncher
¶
Source code in lutris/services/origin.py
class OriginLauncher:
manifests_paths = "ProgramData/Origin/LocalContent"
def __init__(self, prefix_path):
self.prefix_path = prefix_path
def iter_manifests(self):
manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
if not os.path.exists(manifests_path):
logger.warning("No directory in %s", manifests_path)
return
for game_folder in os.listdir(manifests_path):
for manifest in os.listdir(os.path.join(manifests_path, game_folder)):
if not manifest.endswith(".mfst"):
continue
with open(os.path.join(manifests_path, game_folder, manifest), encoding="utf-8") as manifest_file:
manifest_content = manifest_file.read()
parsed_url = urllib.parse.urlparse(manifest_content)
parsed_data = dict(urllib.parse.parse_qsl(parsed_url.query))
yield parsed_data
manifests_paths
¶
__init__(self, prefix_path)
special
¶
Source code in lutris/services/origin.py
def __init__(self, prefix_path):
self.prefix_path = prefix_path
iter_manifests(self)
¶
Source code in lutris/services/origin.py
def iter_manifests(self):
manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
if not os.path.exists(manifests_path):
logger.warning("No directory in %s", manifests_path)
return
for game_folder in os.listdir(manifests_path):
for manifest in os.listdir(os.path.join(manifests_path, game_folder)):
if not manifest.endswith(".mfst"):
continue
with open(os.path.join(manifests_path, game_folder, manifest), encoding="utf-8") as manifest_file:
manifest_content = manifest_file.read()
parsed_url = urllib.parse.urlparse(manifest_content)
parsed_data = dict(urllib.parse.parse_qsl(parsed_url.query))
yield parsed_data
OriginPackArtLarge (OriginPackArtSmall)
¶
OriginPackArtMedium (OriginPackArtSmall)
¶
OriginPackArtSmall (ServiceMedia)
¶
Source code in lutris/services/origin.py
class OriginPackArtSmall(ServiceMedia):
service = "origin"
file_pattern = "%s.jpg"
size = (63, 89)
dest_path = os.path.join(settings.CACHE_DIR, "origin/pack-art-small")
api_field = "packArtSmall"
def get_media_url(self, details):
return details["imageServer"] + details["i18n"][self.api_field]
OriginService (OnlineService)
¶
Service class for EA Origin
Source code in lutris/services/origin.py
class OriginService(OnlineService):
"""Service class for EA Origin"""
id = "origin"
name = _("Origin")
icon = "origin"
client_installer = "origin"
runner = "wine"
online = True
medias = {
"packArtSmall": OriginPackArtSmall,
"packArtMedium": OriginPackArtMedium,
"packArtLarge": OriginPackArtLarge,
}
default_format = "packArtMedium"
cache_path = os.path.join(settings.CACHE_DIR, "origin/cache/")
cookies_path = os.path.join(settings.CACHE_DIR, "origin/cookies")
token_path = os.path.join(settings.CACHE_DIR, "origin/auth_token")
redirect_uri = "https://www.origin.com/views/login.html"
login_url = (
"https://accounts.ea.com/connect/auth"
"?response_type=code&client_id=ORIGIN_SPA_ID&display=originXWeb/login"
"&locale=en_US&release_type=prod"
"&redirect_uri=%s"
) % redirect_uri
is_loading = False
def __init__(self):
super().__init__()
self.session = requests.session()
self.access_token = self.load_access_token()
@property
def api_url(self):
return "https://api%s.origin.com" % random.randint(1, 4)
def run(self):
db_game = get_game_by_field(self.client_installer, "slug")
game = Game(db_game["id"])
game.emit("game-launch")
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
def is_connected(self):
return bool(self.access_token)
def login_callback(self, url):
self.fetch_access_token()
self.emit("service-login")
def fetch_access_token(self):
token_data = self.get_access_token()
if not token_data:
raise RuntimeError("Failed to get access token")
with open(self.token_path, "w", encoding='utf-8') as token_file:
token_file.write(json.dumps(token_data, indent=2))
self.access_token = self.load_access_token()
def load_access_token(self):
if not os.path.exists(self.token_path):
return ""
with open(self.token_path) as token_file:
token_data = json.load(token_file)
return token_data.get("access_token", "")
def get_access_token(self):
"""Request an access token from EA"""
response = self.session.get(
"https://accounts.ea.com/connect/auth",
params={
"client_id": "ORIGIN_JS_SDK",
"response_type": "token",
"redirect_uri": "nucleus:rest",
"prompt": "none"
},
cookies=self.load_cookies()
)
response.raise_for_status()
token_data = response.json()
return token_data
def _request_identity(self):
response = self.session.get(
"https://gateway.ea.com/proxy/identity/pids/me",
cookies=self.load_cookies(),
headers=self.get_auth_headers()
)
return response.json()
def get_identity(self):
"""Request the user info"""
identity_data = self._request_identity()
if identity_data.get('error') == "invalid_access_token":
logger.warning("Refreshing Origin access token")
self.fetch_access_token()
identity_data = self._request_identity()
elif identity_data.get("error"):
raise RuntimeError(
"%s (Error code: %s)" % (identity_data["error"], identity_data["error_number"])
)
if 'error' in identity_data:
raise RuntimeError(identity_data["error"])
try:
user_id = identity_data["pid"]["pidId"]
except KeyError:
logger.error("Can't read user ID from %s", identity_data)
raise
persona_id_response = self.session.get(
"{}/atom/users?userIds={}".format(self.api_url, user_id),
headers=self.get_auth_headers()
)
content = persona_id_response.text
origin_account_info = ElementTree.fromstring(content)
persona_id = origin_account_info.find("user").find("personaId").text
user_name = origin_account_info.find("user").find("EAID").text
return str(user_id), str(persona_id), str(user_name)
def load(self):
if self.is_loading:
logger.warning("Origin games are already loading")
return
user_id, _persona_id, _user_name = self.get_identity()
self.is_loading = True
games = self.get_library(user_id)
logger.info("Retrieved %s games from Origin library", len(games))
origin_games = []
for game in games:
origin_game = OriginGame.new_from_api(game)
origin_game.save()
origin_games.append(origin_game)
self.is_loading = False
return origin_games
def get_library(self, user_id):
"""Load Origin library"""
offers = []
for entitlement in self.get_entitlements(user_id):
if entitlement["offerType"] != "basegame":
continue
offer_id = entitlement["offerId"]
offer = self.get_offer(offer_id)
offers.append(offer)
return offers
def get_offer(self, offer_id):
"""Load offer details from Origin"""
url = "{}/ecommerce2/public/supercat/{}/{}".format(self.api_url, offer_id, "en_US")
response = self.session.get(url, headers=self.get_auth_headers())
return response.json()
def get_entitlements(self, user_id):
"""Request the user's entitlements"""
url = "%s/ecommerce2/consolidatedentitlements/%s?machine_hash=1" % (
self.api_url,
user_id
)
headers = self.get_auth_headers()
headers["Accept"] = "application/vnd.origin.v3+json; x-cache/force-write"
response = self.session.get(url, headers=headers)
data = response.json()
return data["entitlements"]
def get_auth_headers(self):
"""Return headers needed to authenticate HTTP requests"""
if not self.access_token:
raise RuntimeError("User not authenticated to Origin")
return {
"Authorization": "Bearer %s" % self.access_token,
"AuthToken": self.access_token,
"X-AuthToken": self.access_token
}
def add_installed_games(self):
origin_game = get_game_by_field("origin", "slug")
if not origin_game:
logger.error("Origin is not installed")
origin_prefix = origin_game["directory"].split("drive_c")[0]
if not os.path.exists(os.path.join(origin_prefix, "drive_c")):
logger.error("Invalid install of Origin at %s", origin_prefix)
return
origin_launcher = OriginLauncher(origin_prefix)
installed_games = 0
for manifest in origin_launcher.iter_manifests():
self.install_from_origin(origin_game, manifest)
installed_games += 1
logger.debug("Installed %s Origin games", installed_games)
def install_from_origin(self, origin_game, manifest):
offer_id = manifest["id"].split("@")[0]
logger.debug("Installing Origin game %s", offer_id)
service_game = ServiceGameCollection.get_game("origin", offer_id)
if not service_game:
logger.error("Aborting install, %s is not present in the game library.", offer_id)
return
lutris_game_id = slugify(service_game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig(game_config_id=origin_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(manifest["id"])
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner=origin_game["runner"],
slug=slugify(service_game["name"]),
directory=origin_game["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=offer_id,
)
return game_id
def generate_installer(self, db_game, origin_db_game):
origin_game = Game(origin_db_game["id"])
origin_exe = origin_game.config.game_config["exe"]
if not os.path.isabs(origin_exe):
origin_exe = os.path.join(origin_game.config.game_config["prefix"], origin_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": get_launch_arguments(db_game["appid"]),
},
"installer": [
{"task": {
"name": "wineexec",
"executable": origin_exe,
"args": get_launch_arguments(db_game["appid"], "download"),
"prefix": origin_game.config.game_config["prefix"],
"description": (
"Origin will now open and install %s." % db_game["name"]
)
}}
]
}
}
def install(self, db_game):
origin_game = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not origin_game or not origin_game["installed"]:
logger.warning("Installing the Origin client")
installers = get_installers(game_slug=self.client_installer)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, origin_game)],
service=self,
appid=db_game["appid"]
)
api_url
property
readonly
¶
cache_path
¶
client_installer
¶
cookies_path
¶
default_format
¶
icon
¶
id
¶
is_loading
¶
login_url
¶
medias
¶
name
¶
online
¶
redirect_uri
¶
runner
¶
token_path
¶
__init__(self)
special
¶
Source code in lutris/services/origin.py
def __init__(self):
super().__init__()
self.session = requests.session()
self.access_token = self.load_access_token()
add_installed_games(self)
¶
Services can implement this method to scan for locally installed games and add them to lutris.
Source code in lutris/services/origin.py
def add_installed_games(self):
origin_game = get_game_by_field("origin", "slug")
if not origin_game:
logger.error("Origin is not installed")
origin_prefix = origin_game["directory"].split("drive_c")[0]
if not os.path.exists(os.path.join(origin_prefix, "drive_c")):
logger.error("Invalid install of Origin at %s", origin_prefix)
return
origin_launcher = OriginLauncher(origin_prefix)
installed_games = 0
for manifest in origin_launcher.iter_manifests():
self.install_from_origin(origin_game, manifest)
installed_games += 1
logger.debug("Installed %s Origin games", installed_games)
fetch_access_token(self)
¶
Source code in lutris/services/origin.py
def fetch_access_token(self):
token_data = self.get_access_token()
if not token_data:
raise RuntimeError("Failed to get access token")
with open(self.token_path, "w", encoding='utf-8') as token_file:
token_file.write(json.dumps(token_data, indent=2))
self.access_token = self.load_access_token()
generate_installer(self, db_game, origin_db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/origin.py
def generate_installer(self, db_game, origin_db_game):
origin_game = Game(origin_db_game["id"])
origin_exe = origin_game.config.game_config["exe"]
if not os.path.isabs(origin_exe):
origin_exe = os.path.join(origin_game.config.game_config["prefix"], origin_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": get_launch_arguments(db_game["appid"]),
},
"installer": [
{"task": {
"name": "wineexec",
"executable": origin_exe,
"args": get_launch_arguments(db_game["appid"], "download"),
"prefix": origin_game.config.game_config["prefix"],
"description": (
"Origin will now open and install %s." % db_game["name"]
)
}}
]
}
}
get_access_token(self)
¶
Request an access token from EA
Source code in lutris/services/origin.py
def get_access_token(self):
"""Request an access token from EA"""
response = self.session.get(
"https://accounts.ea.com/connect/auth",
params={
"client_id": "ORIGIN_JS_SDK",
"response_type": "token",
"redirect_uri": "nucleus:rest",
"prompt": "none"
},
cookies=self.load_cookies()
)
response.raise_for_status()
token_data = response.json()
return token_data
get_auth_headers(self)
¶
Return headers needed to authenticate HTTP requests
Source code in lutris/services/origin.py
def get_auth_headers(self):
"""Return headers needed to authenticate HTTP requests"""
if not self.access_token:
raise RuntimeError("User not authenticated to Origin")
return {
"Authorization": "Bearer %s" % self.access_token,
"AuthToken": self.access_token,
"X-AuthToken": self.access_token
}
get_entitlements(self, user_id)
¶
Request the user's entitlements
Source code in lutris/services/origin.py
def get_entitlements(self, user_id):
"""Request the user's entitlements"""
url = "%s/ecommerce2/consolidatedentitlements/%s?machine_hash=1" % (
self.api_url,
user_id
)
headers = self.get_auth_headers()
headers["Accept"] = "application/vnd.origin.v3+json; x-cache/force-write"
response = self.session.get(url, headers=headers)
data = response.json()
return data["entitlements"]
get_identity(self)
¶
Request the user info
Source code in lutris/services/origin.py
def get_identity(self):
"""Request the user info"""
identity_data = self._request_identity()
if identity_data.get('error') == "invalid_access_token":
logger.warning("Refreshing Origin access token")
self.fetch_access_token()
identity_data = self._request_identity()
elif identity_data.get("error"):
raise RuntimeError(
"%s (Error code: %s)" % (identity_data["error"], identity_data["error_number"])
)
if 'error' in identity_data:
raise RuntimeError(identity_data["error"])
try:
user_id = identity_data["pid"]["pidId"]
except KeyError:
logger.error("Can't read user ID from %s", identity_data)
raise
persona_id_response = self.session.get(
"{}/atom/users?userIds={}".format(self.api_url, user_id),
headers=self.get_auth_headers()
)
content = persona_id_response.text
origin_account_info = ElementTree.fromstring(content)
persona_id = origin_account_info.find("user").find("personaId").text
user_name = origin_account_info.find("user").find("EAID").text
return str(user_id), str(persona_id), str(user_name)
get_library(self, user_id)
¶
Load Origin library
Source code in lutris/services/origin.py
def get_library(self, user_id):
"""Load Origin library"""
offers = []
for entitlement in self.get_entitlements(user_id):
if entitlement["offerType"] != "basegame":
continue
offer_id = entitlement["offerId"]
offer = self.get_offer(offer_id)
offers.append(offer)
return offers
get_offer(self, offer_id)
¶
Load offer details from Origin
Source code in lutris/services/origin.py
def get_offer(self, offer_id):
"""Load offer details from Origin"""
url = "{}/ecommerce2/public/supercat/{}/{}".format(self.api_url, offer_id, "en_US")
response = self.session.get(url, headers=self.get_auth_headers())
return response.json()
install(self, db_game)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/origin.py
def install(self, db_game):
origin_game = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not origin_game or not origin_game["installed"]:
logger.warning("Installing the Origin client")
installers = get_installers(game_slug=self.client_installer)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, origin_game)],
service=self,
appid=db_game["appid"]
)
install_from_origin(self, origin_game, manifest)
¶
Source code in lutris/services/origin.py
def install_from_origin(self, origin_game, manifest):
offer_id = manifest["id"].split("@")[0]
logger.debug("Installing Origin game %s", offer_id)
service_game = ServiceGameCollection.get_game("origin", offer_id)
if not service_game:
logger.error("Aborting install, %s is not present in the game library.", offer_id)
return
lutris_game_id = slugify(service_game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig(game_config_id=origin_game["configpath"]).game_level
game_config["game"]["args"] = get_launch_arguments(manifest["id"])
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner=origin_game["runner"],
slug=slugify(service_game["name"]),
directory=origin_game["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=offer_id,
)
return game_id
is_connected(self)
¶
Source code in lutris/services/origin.py
def is_connected(self):
return bool(self.access_token)
is_launchable(self)
¶
Source code in lutris/services/origin.py
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
load(self)
¶
Source code in lutris/services/origin.py
def load(self):
if self.is_loading:
logger.warning("Origin games are already loading")
return
user_id, _persona_id, _user_name = self.get_identity()
self.is_loading = True
games = self.get_library(user_id)
logger.info("Retrieved %s games from Origin library", len(games))
origin_games = []
for game in games:
origin_game = OriginGame.new_from_api(game)
origin_game.save()
origin_games.append(origin_game)
self.is_loading = False
return origin_games
load_access_token(self)
¶
Source code in lutris/services/origin.py
def load_access_token(self):
if not os.path.exists(self.token_path):
return ""
with open(self.token_path) as token_file:
token_data = json.load(token_file)
return token_data.get("access_token", "")
login_callback(self, url)
¶
Source code in lutris/services/origin.py
def login_callback(self, url):
self.fetch_access_token()
self.emit("service-login")
run(self)
¶
Override this method to run a launcher
Source code in lutris/services/origin.py
def run(self):
db_game = get_game_by_field(self.client_installer, "slug")
game = Game(db_game["id"])
game.emit("game-launch")
get_launch_arguments(offer_id, action='launch')
¶
Source code in lutris/services/origin.py
def get_launch_arguments(offer_id, action="launch"):
if action == "launch":
return "origin2://game/launch?offerIds=%s&autoDownload=1" % offer_id
if action == "download":
return "origin2://game/download?offerId=%s" % offer_id
scummvm
¶
Legacy ScummVM 'service', has to be ported to the current architecture
ICON
¶
NAME
¶
ONLINE
¶
SCUMMVM_CONFIG_FILE
¶
get_scummvm_games()
¶
Return the available ScummVM games
Source code in lutris/services/scummvm.py
def get_scummvm_games():
"""Return the available ScummVM games"""
if not system.path_exists(SCUMMVM_CONFIG_FILE):
logger.info("No ScummVM config found")
return []
config = ConfigParser()
config.read(SCUMMVM_CONFIG_FILE)
config_sections = config.sections()
for section in config_sections:
if section == "scummvm":
continue
scummvm_id = section
name = re.split(r" \(.*\)$", config[section]["description"])[0]
path = config[section]["path"]
yield (scummvm_id, name, path)
service_game
¶
Service game module
PGA_DB
¶
ServiceGame
¶
Representation of a game from a 3rd party service
Source code in lutris/services/service_game.py
class ServiceGame:
"""Representation of a game from a 3rd party service"""
service = NotImplemented
installer_slug = NotImplemented
medias = (ServiceMedia, )
def __init__(self):
self.appid = None # External ID of the game on the 3rd party service
self.game_id = None # Internal Lutris ID
self.runner = None # Name of the runner
self.name = None # Name
self.slug = None # Game slug
self.lutris_slug = None # Slug used by the lutris website
self.logo = None # Game logo
self.icon = None # Game icon
self.details = None # Additional details for the game
def save(self):
"""Save this game to database"""
game_data = {
"service": self.service,
"appid": self.appid,
"name": self.name,
"slug": self.slug,
"lutris_slug": self.lutris_slug,
"icon": self.icon,
"logo": self.logo,
"details": str(self.details),
}
existing_game = ServiceGameCollection.get_game(self.service, self.appid)
if existing_game:
sql.db_update(PGA_DB, "service_games", game_data, {"id": existing_game["id"]})
else:
sql.db_insert(PGA_DB, "service_games", game_data)
installer_slug
¶
medias
¶
service
¶
__init__(self)
special
¶
Source code in lutris/services/service_game.py
def __init__(self):
self.appid = None # External ID of the game on the 3rd party service
self.game_id = None # Internal Lutris ID
self.runner = None # Name of the runner
self.name = None # Name
self.slug = None # Game slug
self.lutris_slug = None # Slug used by the lutris website
self.logo = None # Game logo
self.icon = None # Game icon
self.details = None # Additional details for the game
save(self)
¶
Save this game to database
Source code in lutris/services/service_game.py
def save(self):
"""Save this game to database"""
game_data = {
"service": self.service,
"appid": self.appid,
"name": self.name,
"slug": self.slug,
"lutris_slug": self.lutris_slug,
"icon": self.icon,
"logo": self.logo,
"details": str(self.details),
}
existing_game = ServiceGameCollection.get_game(self.service, self.appid)
if existing_game:
sql.db_update(PGA_DB, "service_games", game_data, {"id": existing_game["id"]})
else:
sql.db_insert(PGA_DB, "service_games", game_data)
service_media
¶
PGA_DB
¶
ServiceMedia
¶
Information about the service's media format
Source code in lutris/services/service_media.py
class ServiceMedia:
"""Information about the service's media format"""
service = NotImplemented
size = NotImplemented
source = "remote" # set to local if the files don't need to be downloaded
visible = True # This media should be displayed as an option in the UI
small_size = None
dest_path = None
file_pattern = NotImplemented
api_field = NotImplemented
url_pattern = "%s"
def __init__(self):
if self.dest_path and not system.path_exists(self.dest_path):
os.makedirs(self.dest_path)
def get_filename(self, slug):
return self.file_pattern % slug
def get_absolute_path(self, slug):
"""Return the abolute path of a local media"""
return os.path.join(self.dest_path, self.get_filename(slug))
def exists(self, slug):
"""Whether the icon for the specified slug exists locally"""
return system.path_exists(self.get_absolute_path(slug))
def get_pixbuf_for_game(self, slug, is_installed=True):
image_abspath = self.get_absolute_path(slug)
return get_pixbuf(image_abspath, self.size, fallback=get_default_icon(self.size), is_installed=is_installed)
def get_media_url(self, details):
if self.api_field not in details:
logger.warning("No field '%s' in API game %s", self.api_field, details)
return
if not details[self.api_field]:
return
return self.url_pattern % details[self.api_field]
def get_media_urls(self):
"""Return URLs for icons and logos from a service"""
if self.source == "local":
return {}
service_games = ServiceGameCollection.get_for_service(self.service)
medias = {}
for game in service_games:
if not game["details"]:
continue
details = json.loads(game["details"])
media_url = self.get_media_url(details)
if not media_url:
continue
medias[game["slug"]] = media_url
return medias
def download(self, slug, url):
"""Downloads the banner if not present"""
if not url:
return
cache_path = os.path.join(self.dest_path, self.get_filename(slug))
if system.path_exists(cache_path, exclude_empty=True):
return
if system.path_exists(cache_path):
cache_stats = os.stat(cache_path)
# Empty files have a life time between 1 and 2 weeks, retry them after
if time.time() - cache_stats.st_mtime < 3600 * 24 * random.choice(range(7, 15)):
return cache_path
os.unlink(cache_path)
try:
return download_file(url, cache_path, raise_errors=True)
except HTTPError as ex:
logger.error("Failed to download %s: %s", url, ex)
def render(self):
"""Used if the media requires extra processing"""
api_field
¶
dest_path
¶
file_pattern
¶
service
¶
size
¶
small_size
¶
source
¶
url_pattern
¶
visible
¶
__init__(self)
special
¶
Source code in lutris/services/service_media.py
def __init__(self):
if self.dest_path and not system.path_exists(self.dest_path):
os.makedirs(self.dest_path)
download(self, slug, url)
¶
Downloads the banner if not present
Source code in lutris/services/service_media.py
def download(self, slug, url):
"""Downloads the banner if not present"""
if not url:
return
cache_path = os.path.join(self.dest_path, self.get_filename(slug))
if system.path_exists(cache_path, exclude_empty=True):
return
if system.path_exists(cache_path):
cache_stats = os.stat(cache_path)
# Empty files have a life time between 1 and 2 weeks, retry them after
if time.time() - cache_stats.st_mtime < 3600 * 24 * random.choice(range(7, 15)):
return cache_path
os.unlink(cache_path)
try:
return download_file(url, cache_path, raise_errors=True)
except HTTPError as ex:
logger.error("Failed to download %s: %s", url, ex)
exists(self, slug)
¶
Whether the icon for the specified slug exists locally
Source code in lutris/services/service_media.py
def exists(self, slug):
"""Whether the icon for the specified slug exists locally"""
return system.path_exists(self.get_absolute_path(slug))
get_absolute_path(self, slug)
¶
Return the abolute path of a local media
Source code in lutris/services/service_media.py
def get_absolute_path(self, slug):
"""Return the abolute path of a local media"""
return os.path.join(self.dest_path, self.get_filename(slug))
get_filename(self, slug)
¶
Source code in lutris/services/service_media.py
def get_filename(self, slug):
return self.file_pattern % slug
get_media_url(self, details)
¶
Source code in lutris/services/service_media.py
def get_media_url(self, details):
if self.api_field not in details:
logger.warning("No field '%s' in API game %s", self.api_field, details)
return
if not details[self.api_field]:
return
return self.url_pattern % details[self.api_field]
get_media_urls(self)
¶
Return URLs for icons and logos from a service
Source code in lutris/services/service_media.py
def get_media_urls(self):
"""Return URLs for icons and logos from a service"""
if self.source == "local":
return {}
service_games = ServiceGameCollection.get_for_service(self.service)
medias = {}
for game in service_games:
if not game["details"]:
continue
details = json.loads(game["details"])
media_url = self.get_media_url(details)
if not media_url:
continue
medias[game["slug"]] = media_url
return medias
get_pixbuf_for_game(self, slug, is_installed=True)
¶
Source code in lutris/services/service_media.py
def get_pixbuf_for_game(self, slug, is_installed=True):
image_abspath = self.get_absolute_path(slug)
return get_pixbuf(image_abspath, self.size, fallback=get_default_icon(self.size), is_installed=is_installed)
render(self)
¶
Used if the media requires extra processing
Source code in lutris/services/service_media.py
def render(self):
"""Used if the media requires extra processing"""
steam
¶
Steam service
SteamBanner (ServiceMedia)
¶
Source code in lutris/services/steam.py
class SteamBanner(ServiceMedia):
service = "steam"
size = (184, 69)
dest_path = os.path.join(settings.CACHE_DIR, "steam/banners")
file_pattern = "%s.jpg"
api_field = "appid"
url_pattern = "http://cdn.akamai.steamstatic.com/steam/apps/%s/capsule_184x69.jpg"
SteamBannerLarge (ServiceMedia)
¶
Source code in lutris/services/steam.py
class SteamBannerLarge(ServiceMedia):
service = "steam"
size = (460, 215)
dest_path = os.path.join(settings.CACHE_DIR, "steam/header")
file_pattern = "%s.jpg"
api_field = "appid"
url_pattern = "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg"
SteamCover (ServiceMedia)
¶
Source code in lutris/services/steam.py
class SteamCover(ServiceMedia):
service = "steam"
size = (200, 300)
dest_path = os.path.join(settings.CACHE_DIR, "steam/covers")
file_pattern = "%s.jpg"
api_field = "appid"
url_pattern = "http://cdn.steamstatic.com/steam/apps/%s/library_600x900.jpg"
SteamGame (ServiceGame)
¶
ServiceGame for Steam games
Source code in lutris/services/steam.py
class SteamGame(ServiceGame):
"""ServiceGame for Steam games"""
service = "steam"
installer_slug = "steam"
runner = "steam"
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
"""Return a Steam game instance from an AppManifest"""
game = cls()
game.appid = steam_game["appid"]
game.game_id = steam_game["appid"]
game.name = steam_game["name"]
game.slug = slugify(steam_game["name"])
game.runner = cls.runner
game.details = json.dumps(steam_game)
return game
installer_slug
¶
runner
¶
service
¶
new_from_steam_game(steam_game, game_id=None)
classmethod
¶
Return a Steam game instance from an AppManifest
Source code in lutris/services/steam.py
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
"""Return a Steam game instance from an AppManifest"""
game = cls()
game.appid = steam_game["appid"]
game.game_id = steam_game["appid"]
game.name = steam_game["name"]
game.slug = slugify(steam_game["name"])
game.runner = cls.runner
game.details = json.dumps(steam_game)
return game
SteamService (BaseService)
¶
Source code in lutris/services/steam.py
class SteamService(BaseService):
id = "steam"
name = _("Steam")
icon = "steam-client"
medias = {
"banner": SteamBanner,
"banner_large": SteamBannerLarge,
"cover": SteamCover,
}
default_format = "banner"
is_loading = False
runner = "steam"
excluded_appids = [
"221410", # Steam for Linux
"228980", # Steamworks Common Redistributables
"1070560", # Steam Linux Runtime
]
game_class = SteamGame
def load(self):
"""Return importable Steam games"""
if self.is_loading:
logger.warning("Steam games are already loading")
return
self.is_loading = True
steamid = get_user_steam_id()
if not steamid:
logger.error("Unable to find SteamID from Steam config")
return
steam_games = get_steam_library(steamid)
if not steam_games:
raise RuntimeError(_("Failed to load games. Check that your profile is set to public during the sync."))
for steam_game in steam_games:
if steam_game["appid"] in self.excluded_appids:
continue
game = self.game_class.new_from_steam_game(steam_game)
game.save()
self.match_games()
self.is_loading = False
return steam_games
def get_installer_files(self, installer, installer_file_id):
steam_uri = "$STEAM:%s:."
appid = str(installer.script["game"]["appid"])
return [
InstallerFile(installer.game_slug, "steam_game", {
"url": steam_uri % appid,
"filename": appid
})
]
def install_from_steam(self, manifest):
"""Create a new Lutris game based on an existing Steam install"""
if not manifest.is_installed():
return
appid = manifest.steamid
if appid in self.excluded_appids:
return
service_game = ServiceGameCollection.get_game(self.id, appid)
if not service_game:
return
lutris_game_id = "%s-%s" % (self.id, appid)
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig().game_level
game_config["game"]["appid"] = appid
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner="steam",
slug=slugify(service_game["name"]),
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
platform="Linux",
service=self.id,
service_id=appid,
)
return game_id
@property
def steamapps_paths(self):
return get_steamapps_paths()
def add_installed_games(self):
"""Syncs installed Steam games with Lutris"""
installed_appids = []
for steamapps_path in self.steamapps_paths:
for appmanifest_file in get_appmanifests(steamapps_path):
app_manifest_path = os.path.join(steamapps_path, appmanifest_file)
app_manifest = AppManifest(app_manifest_path)
installed_appids.append(app_manifest.steamid)
self.install_from_steam(app_manifest)
db_games = get_games(filters={"runner": "steam"})
for db_game in db_games:
steam_game = Game(db_game["id"])
try:
appid = steam_game.config.game_level["game"]["appid"]
except KeyError:
logger.warning("Steam game %s has no AppID")
continue
if appid not in installed_appids:
steam_game.remove(no_signal=True)
db_appids = defaultdict(list)
db_games = get_games(filters={"service": "steam"})
for db_game in db_games:
db_appids[db_game["service_id"]].append(db_game["id"])
for appid in db_appids:
game_ids = db_appids[appid]
if len(game_ids) == 1:
continue
for game_id in game_ids:
steam_game = Game(game_id)
if not steam_game.playtime:
steam_game.remove(no_signal=True)
steam_game.delete()
def generate_installer(self, db_game):
"""Generate a basic Steam installer"""
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"game": {"appid": db_game["appid"]}
}
}
def install(self, db_game):
appid = db_game["appid"]
db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
existing_game = self.match_existing_game(db_games, appid)
if existing_game:
logger.debug("Found steam game: %s", existing_game)
game = Game(existing_game.id)
game.save()
return
service_installers = self.get_installers_from_api(appid)
if not service_installers:
service_installers = [self.generate_installer(db_game)]
application = Gio.Application.get_default()
application.show_installer_window(service_installers, service=self, appid=appid)
default_format
¶
excluded_appids
¶
icon
¶
id
¶
is_loading
¶
medias
¶
name
¶
runner
¶
steamapps_paths
property
readonly
¶
game_class (ServiceGame)
¶
ServiceGame for Steam games
Source code in lutris/services/steam.py
class SteamGame(ServiceGame):
"""ServiceGame for Steam games"""
service = "steam"
installer_slug = "steam"
runner = "steam"
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
"""Return a Steam game instance from an AppManifest"""
game = cls()
game.appid = steam_game["appid"]
game.game_id = steam_game["appid"]
game.name = steam_game["name"]
game.slug = slugify(steam_game["name"])
game.runner = cls.runner
game.details = json.dumps(steam_game)
return game
installer_slug
¶
runner
¶
service
¶
new_from_steam_game(steam_game, game_id=None)
classmethod
¶
Return a Steam game instance from an AppManifest
Source code in lutris/services/steam.py
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
"""Return a Steam game instance from an AppManifest"""
game = cls()
game.appid = steam_game["appid"]
game.game_id = steam_game["appid"]
game.name = steam_game["name"]
game.slug = slugify(steam_game["name"])
game.runner = cls.runner
game.details = json.dumps(steam_game)
return game
add_installed_games(self)
¶
Syncs installed Steam games with Lutris
Source code in lutris/services/steam.py
def add_installed_games(self):
"""Syncs installed Steam games with Lutris"""
installed_appids = []
for steamapps_path in self.steamapps_paths:
for appmanifest_file in get_appmanifests(steamapps_path):
app_manifest_path = os.path.join(steamapps_path, appmanifest_file)
app_manifest = AppManifest(app_manifest_path)
installed_appids.append(app_manifest.steamid)
self.install_from_steam(app_manifest)
db_games = get_games(filters={"runner": "steam"})
for db_game in db_games:
steam_game = Game(db_game["id"])
try:
appid = steam_game.config.game_level["game"]["appid"]
except KeyError:
logger.warning("Steam game %s has no AppID")
continue
if appid not in installed_appids:
steam_game.remove(no_signal=True)
db_appids = defaultdict(list)
db_games = get_games(filters={"service": "steam"})
for db_game in db_games:
db_appids[db_game["service_id"]].append(db_game["id"])
for appid in db_appids:
game_ids = db_appids[appid]
if len(game_ids) == 1:
continue
for game_id in game_ids:
steam_game = Game(game_id)
if not steam_game.playtime:
steam_game.remove(no_signal=True)
steam_game.delete()
generate_installer(self, db_game)
¶
Generate a basic Steam installer
Source code in lutris/services/steam.py
def generate_installer(self, db_game):
"""Generate a basic Steam installer"""
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"game": {"appid": db_game["appid"]}
}
}
get_installer_files(self, installer, installer_file_id)
¶
Source code in lutris/services/steam.py
def get_installer_files(self, installer, installer_file_id):
steam_uri = "$STEAM:%s:."
appid = str(installer.script["game"]["appid"])
return [
InstallerFile(installer.game_slug, "steam_game", {
"url": steam_uri % appid,
"filename": appid
})
]
install(self, db_game)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/steam.py
def install(self, db_game):
appid = db_game["appid"]
db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
existing_game = self.match_existing_game(db_games, appid)
if existing_game:
logger.debug("Found steam game: %s", existing_game)
game = Game(existing_game.id)
game.save()
return
service_installers = self.get_installers_from_api(appid)
if not service_installers:
service_installers = [self.generate_installer(db_game)]
application = Gio.Application.get_default()
application.show_installer_window(service_installers, service=self, appid=appid)
install_from_steam(self, manifest)
¶
Create a new Lutris game based on an existing Steam install
Source code in lutris/services/steam.py
def install_from_steam(self, manifest):
"""Create a new Lutris game based on an existing Steam install"""
if not manifest.is_installed():
return
appid = manifest.steamid
if appid in self.excluded_appids:
return
service_game = ServiceGameCollection.get_game(self.id, appid)
if not service_game:
return
lutris_game_id = "%s-%s" % (self.id, appid)
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
return
game_config = LutrisConfig().game_level
game_config["game"]["appid"] = appid
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=service_game["name"],
runner="steam",
slug=slugify(service_game["name"]),
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
platform="Linux",
service=self.id,
service_id=appid,
)
return game_id
load(self)
¶
Return importable Steam games
Source code in lutris/services/steam.py
def load(self):
"""Return importable Steam games"""
if self.is_loading:
logger.warning("Steam games are already loading")
return
self.is_loading = True
steamid = get_user_steam_id()
if not steamid:
logger.error("Unable to find SteamID from Steam config")
return
steam_games = get_steam_library(steamid)
if not steam_games:
raise RuntimeError(_("Failed to load games. Check that your profile is set to public during the sync."))
for steam_game in steam_games:
if steam_game["appid"] in self.excluded_appids:
continue
game = self.game_class.new_from_steam_game(steam_game)
game.save()
self.match_games()
self.is_loading = False
return steam_games
steamwindows
¶
STEAM_INSTALLER
¶
SteamWindowsService (SteamService)
¶
Source code in lutris/services/steamwindows.py
class SteamWindowsService(SteamService):
id = "steamwindows"
name = _("Steam for Windows")
runner = "wine"
game_class = SteamWindowsGame
client_installer = "steam-wine"
def generate_installer(self, db_game, steam_game):
"""Generate a basic Steam installer"""
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"exe": steam_game.config.game_config["exe"],
"args": "-no-cef-sandbox -applaunch %s" % db_game["appid"],
"prefix": steam_game.config.game_config["prefix"],
}
}
}
def get_steam(self):
db_entry = get_game_by_field(self.client_installer, "installer_slug")
if db_entry:
return Game(db_entry["id"])
def install(self, db_game):
steam_game = self.get_steam()
if not steam_game:
installers = get_installers(
game_slug=self.client_installer,
)
appid = None
else:
installers = [self.generate_installer(db_game, steam_game)]
appid = db_game["appid"]
db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
existing_game = self.match_existing_game(db_games, appid)
if existing_game:
logger.debug("Found steam game: %s", existing_game)
game = Game(existing_game.id)
game.save()
return
application = Gio.Application.get_default()
application.show_installer_window(
installers,
service=self,
appid=appid
)
@property
def steamapps_paths(self):
"""Return steamapps paths"""
steam_game = self.get_steam()
if not steam_game:
return []
dirs = []
steam_path = steam_game.config.game_config["exe"]
steam_data_dir = os.path.dirname(steam_path)
if steam_data_dir:
main_dir = os.path.join(steam_data_dir, "steamapps")
main_dir = system.fix_path_case(main_dir)
if main_dir and os.path.isdir(main_dir):
dirs.append(os.path.abspath(main_dir))
return dirs
client_installer
¶
id
¶
name
¶
runner
¶
steamapps_paths
property
readonly
¶
Return steamapps paths
generate_installer(self, db_game, steam_game)
¶
Generate a basic Steam installer
Source code in lutris/services/steamwindows.py
def generate_installer(self, db_game, steam_game):
"""Generate a basic Steam installer"""
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"exe": steam_game.config.game_config["exe"],
"args": "-no-cef-sandbox -applaunch %s" % db_game["appid"],
"prefix": steam_game.config.game_config["prefix"],
}
}
}
get_steam(self)
¶
Source code in lutris/services/steamwindows.py
def get_steam(self):
db_entry = get_game_by_field(self.client_installer, "installer_slug")
if db_entry:
return Game(db_entry["id"])
install(self, db_game)
¶
Install a service game, or starts the installer of the game.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_game |
dict or str |
Database fields of the game to add, or (for Lutris service only the slug of the game.) |
required |
Returns:
| Type | Description |
|---|---|
str |
The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None. |
Source code in lutris/services/steamwindows.py
def install(self, db_game):
steam_game = self.get_steam()
if not steam_game:
installers = get_installers(
game_slug=self.client_installer,
)
appid = None
else:
installers = [self.generate_installer(db_game, steam_game)]
appid = db_game["appid"]
db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
existing_game = self.match_existing_game(db_games, appid)
if existing_game:
logger.debug("Found steam game: %s", existing_game)
game = Game(existing_game.id)
game.save()
return
application = Gio.Application.get_default()
application.show_installer_window(
installers,
service=self,
appid=appid
)
tosec
¶
TOSEC service Not ready yet
TOSECService (BaseService)
¶
ubisoft
¶
Ubisoft Connect service
UbisoftConnectService (OnlineService)
¶
Service class for Ubisoft Connect
Source code in lutris/services/ubisoft.py
class UbisoftConnectService(OnlineService):
"""Service class for Ubisoft Connect"""
id = "ubisoft"
name = _("Ubisoft Connect")
icon = "ubisoft"
runner = "wine"
client_installer = "ubisoft-connect"
browser_size = (460, 690)
cookies_path = os.path.join(settings.CACHE_DIR, "ubisoft/.auth")
token_path = os.path.join(settings.CACHE_DIR, "ubisoft/.token")
cache_path = os.path.join(settings.CACHE_DIR, "ubisoft/library/")
login_url = consts.LOGIN_URL
redirect_uri = "https://connect.ubisoft.com/change_domain/"
scripts = {
"https://connect.ubisoft.com/ready": (
'window.location.replace("https://connect.ubisoft.com/change_domain/");'
),
"https://connect.ubisoft.com/change_domain/": (
'window.location.replace(localStorage.getItem("PRODloginData") +","+ '
'localStorage.getItem("PRODrememberMe") +"," + localStorage.getItem("PRODlastProfile"));'
)
}
medias = {
"cover": UbisoftCover,
}
default_format = "cover"
is_loading = False
def __init__(self):
super().__init__()
self.client = UbisoftConnectClient(self)
def auth_lost(self):
self.emit("service-logout")
def login_callback(self, credentials):
"""Called after the user has logged in successfully"""
url = credentials[len("https://connect.ubisoft.com/change_domain/"):]
unquoted_url = unquote(url)
storage_jsons = json.loads("[" + unquoted_url + "]")
user_data = self.client.authorise_with_local_storage(storage_jsons)
self.client.set_auth_lost_callback(self.auth_lost)
self.emit("service-login")
return (user_data['userId'], user_data['username'])
def run(self):
db_game = get_game_by_field(self.client_installer, "slug")
game = Game(db_game["id"])
game.emit("game-launch")
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
def is_connected(self):
return self.is_authenticated()
def get_configurations(self):
ubi_game = get_game_by_field("ubisoft-connect", "slug")
if not ubi_game:
return
base_dir = ubi_game["directory"]
configurations_path = os.path.join(
base_dir,
"drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/"
"cache/configuration/configurations"
)
with open(configurations_path, "rb") as config_file:
content = config_file.read()
return content
def load(self):
self.is_loading = True
self.client.authorise_with_stored_credentials(self.load_credentials())
response = self.client.get_club_titles()
games = response['data']['viewer']['ownedGames'].get('nodes', [])
ubi_games = []
for game in games:
if "ownedPlatformGroups" in game:
is_pc = False
for platform_group in game["ownedPlatformGroups"]:
for platform in platform_group:
if platform["type"] == "PC":
is_pc = True
if not is_pc:
continue
ubi_game = UbisoftGame.new_from_api(game)
ubi_game.save()
ubi_games.append(ubi_game)
configuration_data = self.get_configurations()
config_parser = UbisoftParser()
games = []
for game in config_parser.parse_games(configuration_data):
ubi_game = UbisoftGame.new_from_api(game)
ubi_game.save()
ubi_games.append(ubi_game)
self.is_loading = False
return ubi_games
def store_credentials(self, credentials):
with open(self.token_path, "w", encoding='utf-8') as auth_file:
auth_file.write(json.dumps(credentials, indent=2))
def load_credentials(self):
with open(self.token_path) as auth_file:
credentials = json.load(auth_file)
return credentials
def install_from_ubisoft(self, ubisoft_connect, game):
app_name = game["name"]
lutris_game_id = slugify(game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
logger.debug("Ubisoft Connect game %s is already installed", app_name)
return
logger.debug("Installing Ubisoft Connect game %s", app_name)
game_config = LutrisConfig(game_config_id=ubisoft_connect["configpath"]).game_level
game_config["game"]["args"] = f"uplay://launch/{game['appid']}"
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=game["name"],
runner=self.runner,
slug=slugify(game["name"]),
directory=ubisoft_connect["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=game["appid"],
)
return game_id
def add_installed_games(self):
ubisoft_connect = get_game_by_field(self.client_installer, "slug")
if not ubisoft_connect:
logger.warning("Ubisoft Connect not installed")
return
prefix_path = ubisoft_connect["directory"].split("drive_c")[0]
prefix = WinePrefixManager(prefix_path)
for game in ServiceGameCollection.get_for_service(self.id):
details = json.loads(game["details"])
install_path = get_ubisoft_registry(prefix, details.get("registryPath"))
exe = get_ubisoft_registry(prefix, details.get("exe"))
if install_path and exe:
self.install_from_ubisoft(ubisoft_connect, game)
def generate_installer(self, db_game, ubi_db_game):
ubisoft_connect = Game(ubi_db_game["id"])
uc_exe = ubisoft_connect.config.game_config["exe"]
if not os.path.isabs(uc_exe):
uc_exe = os.path.join(ubisoft_connect.config.game_config["prefix"], uc_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": f"uplay://launch/{db_game['appid']}",
},
"installer": [
{"task": {
"name": "wineexec",
"executable": uc_exe,
"args": f"uplay://install/{db_game['appid']}",
"prefix": ubisoft_connect.config.game_config["prefix"],
"description": (
"Ubisoft will now open and install %s. "
"Close Ubisoft Connect to complete the install process."
) % db_game["name"]
}}
]
}
}
def install(self, db_game):
"""Install a game or Ubisoft Connect if not already installed"""
ubisoft_connect = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not ubisoft_connect or not ubisoft_connect["installed"]:
logger.warning("Ubisoft Connect (%s) not installed", self.client_installer)
installers = get_installers(game_slug=self.client_installer)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, ubisoft_connect)],
service=self,
appid=db_game["appid"]
)
browser_size
¶
cache_path
¶
client_installer
¶
cookies_path
¶
default_format
¶
icon
¶
id
¶
is_loading
¶
login_url
¶
medias
¶
name
¶
redirect_uri
¶
runner
¶
scripts
¶
token_path
¶
__init__(self)
special
¶
Source code in lutris/services/ubisoft.py
def __init__(self):
super().__init__()
self.client = UbisoftConnectClient(self)
add_installed_games(self)
¶
Services can implement this method to scan for locally installed games and add them to lutris.
Source code in lutris/services/ubisoft.py
def add_installed_games(self):
ubisoft_connect = get_game_by_field(self.client_installer, "slug")
if not ubisoft_connect:
logger.warning("Ubisoft Connect not installed")
return
prefix_path = ubisoft_connect["directory"].split("drive_c")[0]
prefix = WinePrefixManager(prefix_path)
for game in ServiceGameCollection.get_for_service(self.id):
details = json.loads(game["details"])
install_path = get_ubisoft_registry(prefix, details.get("registryPath"))
exe = get_ubisoft_registry(prefix, details.get("exe"))
if install_path and exe:
self.install_from_ubisoft(ubisoft_connect, game)
auth_lost(self)
¶
Source code in lutris/services/ubisoft.py
def auth_lost(self):
self.emit("service-logout")
generate_installer(self, db_game, ubi_db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/ubisoft.py
def generate_installer(self, db_game, ubi_db_game):
ubisoft_connect = Game(ubi_db_game["id"])
uc_exe = ubisoft_connect.config.game_config["exe"]
if not os.path.isabs(uc_exe):
uc_exe = os.path.join(ubisoft_connect.config.game_config["prefix"], uc_exe)
return {
"name": db_game["name"],
"version": self.name,
"slug": slugify(db_game["name"]) + "-" + self.id,
"game_slug": slugify(db_game["name"]),
"runner": self.runner,
"appid": db_game["appid"],
"script": {
"requires": self.client_installer,
"game": {
"args": f"uplay://launch/{db_game['appid']}",
},
"installer": [
{"task": {
"name": "wineexec",
"executable": uc_exe,
"args": f"uplay://install/{db_game['appid']}",
"prefix": ubisoft_connect.config.game_config["prefix"],
"description": (
"Ubisoft will now open and install %s. "
"Close Ubisoft Connect to complete the install process."
) % db_game["name"]
}}
]
}
}
get_configurations(self)
¶
Source code in lutris/services/ubisoft.py
def get_configurations(self):
ubi_game = get_game_by_field("ubisoft-connect", "slug")
if not ubi_game:
return
base_dir = ubi_game["directory"]
configurations_path = os.path.join(
base_dir,
"drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/"
"cache/configuration/configurations"
)
with open(configurations_path, "rb") as config_file:
content = config_file.read()
return content
install(self, db_game)
¶
Install a game or Ubisoft Connect if not already installed
Source code in lutris/services/ubisoft.py
def install(self, db_game):
"""Install a game or Ubisoft Connect if not already installed"""
ubisoft_connect = get_game_by_field(self.client_installer, "slug")
application = Gio.Application.get_default()
if not ubisoft_connect or not ubisoft_connect["installed"]:
logger.warning("Ubisoft Connect (%s) not installed", self.client_installer)
installers = get_installers(game_slug=self.client_installer)
application.show_installer_window(installers)
else:
application.show_installer_window(
[self.generate_installer(db_game, ubisoft_connect)],
service=self,
appid=db_game["appid"]
)
install_from_ubisoft(self, ubisoft_connect, game)
¶
Source code in lutris/services/ubisoft.py
def install_from_ubisoft(self, ubisoft_connect, game):
app_name = game["name"]
lutris_game_id = slugify(game["name"]) + "-" + self.id
existing_game = get_game_by_field(lutris_game_id, "installer_slug")
if existing_game:
logger.debug("Ubisoft Connect game %s is already installed", app_name)
return
logger.debug("Installing Ubisoft Connect game %s", app_name)
game_config = LutrisConfig(game_config_id=ubisoft_connect["configpath"]).game_level
game_config["game"]["args"] = f"uplay://launch/{game['appid']}"
configpath = write_game_config(lutris_game_id, game_config)
game_id = add_game(
name=game["name"],
runner=self.runner,
slug=slugify(game["name"]),
directory=ubisoft_connect["directory"],
installed=1,
installer_slug=lutris_game_id,
configpath=configpath,
service=self.id,
service_id=game["appid"],
)
return game_id
is_connected(self)
¶
Source code in lutris/services/ubisoft.py
def is_connected(self):
return self.is_authenticated()
is_launchable(self)
¶
Source code in lutris/services/ubisoft.py
def is_launchable(self):
return get_game_by_field(self.client_installer, "slug")
load(self)
¶
Source code in lutris/services/ubisoft.py
def load(self):
self.is_loading = True
self.client.authorise_with_stored_credentials(self.load_credentials())
response = self.client.get_club_titles()
games = response['data']['viewer']['ownedGames'].get('nodes', [])
ubi_games = []
for game in games:
if "ownedPlatformGroups" in game:
is_pc = False
for platform_group in game["ownedPlatformGroups"]:
for platform in platform_group:
if platform["type"] == "PC":
is_pc = True
if not is_pc:
continue
ubi_game = UbisoftGame.new_from_api(game)
ubi_game.save()
ubi_games.append(ubi_game)
configuration_data = self.get_configurations()
config_parser = UbisoftParser()
games = []
for game in config_parser.parse_games(configuration_data):
ubi_game = UbisoftGame.new_from_api(game)
ubi_game.save()
ubi_games.append(ubi_game)
self.is_loading = False
return ubi_games
load_credentials(self)
¶
Source code in lutris/services/ubisoft.py
def load_credentials(self):
with open(self.token_path) as auth_file:
credentials = json.load(auth_file)
return credentials
login_callback(self, credentials)
¶
Called after the user has logged in successfully
Source code in lutris/services/ubisoft.py
def login_callback(self, credentials):
"""Called after the user has logged in successfully"""
url = credentials[len("https://connect.ubisoft.com/change_domain/"):]
unquoted_url = unquote(url)
storage_jsons = json.loads("[" + unquoted_url + "]")
user_data = self.client.authorise_with_local_storage(storage_jsons)
self.client.set_auth_lost_callback(self.auth_lost)
self.emit("service-login")
return (user_data['userId'], user_data['username'])
run(self)
¶
Override this method to run a launcher
Source code in lutris/services/ubisoft.py
def run(self):
db_game = get_game_by_field(self.client_installer, "slug")
game = Game(db_game["id"])
game.emit("game-launch")
store_credentials(self, credentials)
¶
Source code in lutris/services/ubisoft.py
def store_credentials(self, credentials):
with open(self.token_path, "w", encoding='utf-8') as auth_file:
auth_file.write(json.dumps(credentials, indent=2))
UbisoftCover (ServiceMedia)
¶
Ubisoft connect cover art
Source code in lutris/services/ubisoft.py
class UbisoftCover(ServiceMedia):
"""Ubisoft connect cover art"""
service = "ubisoft"
size = (160, 186)
dest_path = os.path.join(settings.CACHE_DIR, "ubisoft/covers")
file_pattern = "%s.jpg"
api_field = "id"
url_pattern = "https://ubiservices.cdn.ubi.com/%s/spaceCardAsset/boxArt_mobile.jpg?imwidth=320"
def get_media_url(self, details):
if self.api_field in details:
return super().get_media_url(details)
return details["thumbImage"]
def download(self, slug, url):
if url.startswith("http"):
return super().download(slug, url)
if not url.endswith(".jpg"):
return
ubi_game = get_game_by_field("ubisoft-connect", "slug")
if not ubi_game:
return
base_dir = ubi_game["directory"]
asset_file = os.path.join(
base_dir,
"drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/cache/assets",
url
)
cache_path = os.path.join(self.dest_path, self.get_filename(slug))
if os.path.exists(asset_file):
shutil.copy(asset_file, cache_path)
else:
logger.warning("No thumbnail in %s", asset_file)
api_field
¶
dest_path
¶
file_pattern
¶
service
¶
size
¶
url_pattern
¶
download(self, slug, url)
¶
Downloads the banner if not present
Source code in lutris/services/ubisoft.py
def download(self, slug, url):
if url.startswith("http"):
return super().download(slug, url)
if not url.endswith(".jpg"):
return
ubi_game = get_game_by_field("ubisoft-connect", "slug")
if not ubi_game:
return
base_dir = ubi_game["directory"]
asset_file = os.path.join(
base_dir,
"drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/cache/assets",
url
)
cache_path = os.path.join(self.dest_path, self.get_filename(slug))
if os.path.exists(asset_file):
shutil.copy(asset_file, cache_path)
else:
logger.warning("No thumbnail in %s", asset_file)
get_media_url(self, details)
¶
Source code in lutris/services/ubisoft.py
def get_media_url(self, details):
if self.api_field in details:
return super().get_media_url(details)
return details["thumbImage"]
UbisoftGame (ServiceGame)
¶
Service game for games from Ubisoft connect
Source code in lutris/services/ubisoft.py
class UbisoftGame(ServiceGame):
"""Service game for games from Ubisoft connect"""
service = "ubisoft"
@classmethod
def new_from_api(cls, payload):
"""Convert an Ubisoft game to a service game"""
service_game = cls()
service_game.appid = payload["spaceId"] or payload["installId"]
service_game.slug = slugify(payload["name"])
service_game.name = payload["name"]
service_game.details = json.dumps(payload)
return service_game
service
¶
new_from_api(payload)
classmethod
¶
Convert an Ubisoft game to a service game
Source code in lutris/services/ubisoft.py
@classmethod
def new_from_api(cls, payload):
"""Convert an Ubisoft game to a service game"""
service_game = cls()
service_game.appid = payload["spaceId"] or payload["installId"]
service_game.slug = slugify(payload["name"])
service_game.name = payload["name"]
service_game.details = json.dumps(payload)
return service_game
xdg
¶
XDG applications service
XDGGame (ServiceGame)
¶
XDG game (Linux game with a desktop launcher)
Source code in lutris/services/xdg.py
class XDGGame(ServiceGame):
"""XDG game (Linux game with a desktop launcher)"""
service = "xdg"
runner = "linux"
installer_slug = "desktopapp"
@staticmethod
def get_app_icon(xdg_app):
"""Return the name of the icon for an XDG app if one if set"""
icon = xdg_app.get_icon()
if not icon:
return ""
return icon.to_string()
@classmethod
def new_from_xdg_app(cls, xdg_app):
"""Create a service game from a XDG entry"""
service_game = cls()
service_game.name = xdg_app.get_display_name()
service_game.icon = cls.get_app_icon(xdg_app)
service_game.appid = get_appid(xdg_app)
service_game.slug = cls.get_slug(xdg_app)
exe, args = cls.get_command_args(xdg_app)
service_game.details = json.dumps({
"exe": exe,
"args": args,
})
return service_game
@staticmethod
def get_command_args(app):
"""Return a tuple with absolute command path and an argument string"""
command = shlex.split(app.get_commandline())
# remove %U etc. and change %% to % in arguments
args = list(map(lambda arg: re.sub("%[^%]", "", arg).replace("%%", "%"), command[1:]))
exe = command[0]
if not exe.startswith("/"):
exe = system.find_executable(exe)
return exe, subprocess.list2cmdline(args)
@staticmethod
def get_slug(xdg_app):
"""Get the slug from the game name"""
return slugify(xdg_app.get_display_name()) or slugify(get_appid(xdg_app))
installer_slug
¶
runner
¶
service
¶
get_app_icon(xdg_app)
staticmethod
¶
Return the name of the icon for an XDG app if one if set
Source code in lutris/services/xdg.py
@staticmethod
def get_app_icon(xdg_app):
"""Return the name of the icon for an XDG app if one if set"""
icon = xdg_app.get_icon()
if not icon:
return ""
return icon.to_string()
get_command_args(app)
staticmethod
¶
Return a tuple with absolute command path and an argument string
Source code in lutris/services/xdg.py
@staticmethod
def get_command_args(app):
"""Return a tuple with absolute command path and an argument string"""
command = shlex.split(app.get_commandline())
# remove %U etc. and change %% to % in arguments
args = list(map(lambda arg: re.sub("%[^%]", "", arg).replace("%%", "%"), command[1:]))
exe = command[0]
if not exe.startswith("/"):
exe = system.find_executable(exe)
return exe, subprocess.list2cmdline(args)
get_slug(xdg_app)
staticmethod
¶
Get the slug from the game name
Source code in lutris/services/xdg.py
@staticmethod
def get_slug(xdg_app):
"""Get the slug from the game name"""
return slugify(xdg_app.get_display_name()) or slugify(get_appid(xdg_app))
new_from_xdg_app(xdg_app)
classmethod
¶
Create a service game from a XDG entry
Source code in lutris/services/xdg.py
@classmethod
def new_from_xdg_app(cls, xdg_app):
"""Create a service game from a XDG entry"""
service_game = cls()
service_game.name = xdg_app.get_display_name()
service_game.icon = cls.get_app_icon(xdg_app)
service_game.appid = get_appid(xdg_app)
service_game.slug = cls.get_slug(xdg_app)
exe, args = cls.get_command_args(xdg_app)
service_game.details = json.dumps({
"exe": exe,
"args": args,
})
return service_game
XDGMedia (ServiceMedia)
¶
XDGService (BaseService)
¶
Source code in lutris/services/xdg.py
class XDGService(BaseService):
id = "xdg"
name = _("Local")
icon = "linux"
online = False
local = True
medias = {
"icon": XDGMedia
}
ignored_games = ("lutris", )
ignored_executables = ("lutris", "steam")
ignored_categories = ("Emulator", "Development", "Utility")
@classmethod
def iter_xdg_games(cls):
"""Iterates through XDG games only"""
for app in Gio.AppInfo.get_all():
if cls._is_importable(app):
yield app
@property
def lutris_games(self):
"""Iterates through Lutris games imported from XDG"""
for game in get_games_where(runner=XDGGame.runner, installer_slug=XDGGame.installer_slug, installed=1):
yield game
@classmethod
def _is_importable(cls, app):
"""Returns whether a XDG game is importable to Lutris"""
appid = get_appid(app)
executable = app.get_executable() or ""
if any(
[
app.get_nodisplay() or app.get_is_hidden(), # App is hidden
not executable, # Check app has an executable
appid.startswith("net.lutris"), # Skip lutris created shortcuts
appid.lower() in map(str.lower, cls.ignored_games), # game blacklisted
executable.lower() in cls.ignored_executables, # exe blacklisted
]
):
return False
# must be in Game category
categories = app.get_categories() or ""
categories = list(filter(None, categories.lower().split(";")))
if "game" not in categories:
return False
# contains a blacklisted category
if bool([category for category in categories if category in map(str.lower, cls.ignored_categories)]):
return False
return True
def match_games(self):
"""XDG games aren't on the lutris website"""
return
def load(self):
"""Return the list of games stored in the XDG menu."""
xdg_games = [XDGGame.new_from_xdg_app(app) for app in self.iter_xdg_games()]
for game in xdg_games:
game.save()
return xdg_games
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
return {
"name": db_game["name"],
"version": "XDG",
"slug": db_game["slug"],
"game_slug": slugify(db_game["name"]),
"runner": "linux",
"script": {
"game": {
"exe": details["exe"],
"args": details["args"],
},
"system": {"disable_runtime": True}
}
}
def get_game_directory(self, installer):
"""Pull install location from installer"""
return os.path.dirname(installer["script"]["game"]["exe"])
icon
¶
id
¶
ignored_categories
¶
ignored_executables
¶
ignored_games
¶
local
¶
lutris_games
property
readonly
¶
Iterates through Lutris games imported from XDG
medias
¶
name
¶
online
¶
generate_installer(self, db_game)
¶
Used to generate an installer from the data returned from the services
Source code in lutris/services/xdg.py
def generate_installer(self, db_game):
details = json.loads(db_game["details"])
return {
"name": db_game["name"],
"version": "XDG",
"slug": db_game["slug"],
"game_slug": slugify(db_game["name"]),
"runner": "linux",
"script": {
"game": {
"exe": details["exe"],
"args": details["args"],
},
"system": {"disable_runtime": True}
}
}
get_game_directory(self, installer)
¶
Pull install location from installer
Source code in lutris/services/xdg.py
def get_game_directory(self, installer):
"""Pull install location from installer"""
return os.path.dirname(installer["script"]["game"]["exe"])
iter_xdg_games()
classmethod
¶
Iterates through XDG games only
Source code in lutris/services/xdg.py
@classmethod
def iter_xdg_games(cls):
"""Iterates through XDG games only"""
for app in Gio.AppInfo.get_all():
if cls._is_importable(app):
yield app
load(self)
¶
Return the list of games stored in the XDG menu.
Source code in lutris/services/xdg.py
def load(self):
"""Return the list of games stored in the XDG menu."""
xdg_games = [XDGGame.new_from_xdg_app(app) for app in self.iter_xdg_games()]
for game in xdg_games:
game.save()
return xdg_games
match_games(self)
¶
XDG games aren't on the lutris website
Source code in lutris/services/xdg.py
def match_games(self):
"""XDG games aren't on the lutris website"""
return
get_appid(app)
¶
Get the appid for the game
Source code in lutris/services/xdg.py
def get_appid(app):
"""Get the appid for the game"""
try:
return os.path.splitext(app.get_id())[0]
except UnicodeDecodeError:
logger.exception(
"Failed to read ID for app %s (non UTF-8 encoding). Reverting to executable name.",
app,
)
return app.get_executable()
settings
¶
Internal settings.
AUTHORS
¶
BANNER_PATH
¶
CACHE_DIR
¶
CONFIG_DIR
¶
CONFIG_FILE
¶
COPYRIGHT
¶
COVERART_PATH
¶
DATA_DIR
¶
DISCORD_CLIENT_ID
¶
DRIVER_HOWTO_URL
¶
GAME_CONFIG_DIR
¶
GAME_URL
¶
ICON_PATH
¶
INSTALLER_REVISION_URL
¶
INSTALLER_URL
¶
PROJECT
¶
RUNNER_DIR
¶
RUNTIME_DIR
¶
RUNTIME_URL
¶
SHADER_CACHE_DIR
¶
SHOW_MEDIA
¶
SITE_URL
¶
STEAM_API_KEY
¶
TMP_PATH
¶
VERSION
¶
read_setting
¶
sio
¶
write_setting
¶
startup
¶
Check to run at program start
check_driver()
¶
Report on the currently running driver
Source code in lutris/startup.py
def check_driver():
"""Report on the currently running driver"""
driver_info = {}
if drivers.is_nvidia():
driver_info = drivers.get_nvidia_driver_info()
# pylint: disable=logging-format-interpolation
logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"]))
gpus = drivers.get_nvidia_gpu_ids()
for gpu_id in gpus:
gpu_info = drivers.get_nvidia_gpu_info(gpu_id)
logger.info("GPU: %s", gpu_info.get("Model"))
elif LINUX_SYSTEM.glxinfo:
# pylint: disable=no-member
if hasattr(LINUX_SYSTEM.glxinfo, "GLX_MESA_query_renderer"):
logger.info(
"Running %s Mesa driver %s on %s",
LINUX_SYSTEM.glxinfo.opengl_vendor,
LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version,
LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.device,
)
else:
logger.warning("glxinfo is not available on your system, unable to detect driver version")
for card in drivers.get_gpus():
# pylint: disable=logging-format-interpolation
try:
logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} ({DRIVER} drivers)".format(**drivers.get_gpu_info(card)))
except KeyError:
logger.error("Unable to get GPU information from '%s'", card)
if drivers.is_outdated():
setting = "hide-outdated-nvidia-driver-warning"
if settings.read_setting(setting) != "True":
DontShowAgainDialog(
setting,
_("Your NVIDIA driver is outdated."),
secondary_message=_(
"You are currently running driver %s which does not "
"fully support all features for Vulkan and DXVK games.\n"
"Please upgrade your driver as described in our "
"<a href='%s'>installation guide</a>"
) % (
driver_info["nvrm"]["version"],
settings.DRIVER_HOWTO_URL,
)
)
check_libs(all_components=False)
¶
Checks that required libraries are installed on the system
Source code in lutris/startup.py
def check_libs(all_components=False):
"""Checks that required libraries are installed on the system"""
missing_libs = LINUX_SYSTEM.get_missing_libs()
if all_components:
components = LINUX_SYSTEM.requirements
else:
components = LINUX_SYSTEM.critical_requirements
missing_vulkan_libs = []
for req in components:
for index, arch in enumerate(LINUX_SYSTEM.runtime_architectures):
for lib in missing_libs[req][index]:
if req == "VULKAN":
missing_vulkan_libs.append(arch)
logger.error("%s %s missing (needed by %s)", arch, lib, req.lower())
if missing_vulkan_libs:
setting = "dismiss-missing-vulkan-library-warning"
if settings.read_setting(setting) != "True":
DontShowAgainDialog(
setting,
_("Missing vulkan libraries"),
secondary_message=_(
"Lutris was unable to detect Vulkan support for "
"the %s architecture.\n"
"This will prevent many games and programs from working.\n"
"To install it, please use the following guide: "
"<a href='%s'>Installing Graphics Drivers</a>"
) % (
_(" and ").join(missing_vulkan_libs),
settings.DRIVER_HOWTO_URL,
)
)
check_vulkan()
¶
Reports if Vulkan is enabled on the system
Source code in lutris/startup.py
def check_vulkan():
"""Reports if Vulkan is enabled on the system"""
if not vkquery.is_vulkan_supported():
logger.warning("Vulkan is not available or your system isn't Vulkan capable")
fill_missing_platforms()
¶
Sets the platform on games where it's missing. This should never happen.
Source code in lutris/startup.py
def fill_missing_platforms():
"""Sets the platform on games where it's missing.
This should never happen.
"""
pga_games = get_games(filters={"installed": 1})
for pga_game in pga_games:
if pga_game.get("platform") or not pga_game["runner"]:
continue
game = Game(game_id=pga_game["id"])
game.set_platform_from_runner()
if game.platform:
logger.info("Platform for %s set to %s", game.name, game.platform)
game.save(save_config=False)
init_dirs()
¶
Creates Lutris directories
Source code in lutris/startup.py
def init_dirs():
"""Creates Lutris directories"""
directories = [
settings.CONFIG_DIR,
os.path.join(settings.CONFIG_DIR, "runners"),
os.path.join(settings.CONFIG_DIR, "games"),
settings.DATA_DIR,
os.path.join(settings.DATA_DIR, "covers"),
settings.ICON_PATH,
os.path.join(settings.CACHE_DIR, "banners"),
os.path.join(settings.CACHE_DIR, "coverart"),
os.path.join(settings.DATA_DIR, "runners"),
os.path.join(settings.DATA_DIR, "lib"),
settings.RUNTIME_DIR,
settings.CACHE_DIR,
settings.SHADER_CACHE_DIR,
os.path.join(settings.CACHE_DIR, "installer"),
os.path.join(settings.CACHE_DIR, "tmp"),
]
for directory in directories:
create_folder(directory)
init_lutris()
¶
Run full initialization of Lutris
Source code in lutris/startup.py
def init_lutris():
"""Run full initialization of Lutris"""
logger.info("Starting Lutris %s", settings.VERSION)
runners.inject_runners(load_json_runners())
# Load runner names and platforms
runners.RUNNER_NAMES = runners.get_runner_names()
runners.RUNNER_PLATFORMS = runners.get_platforms()
init_dirs()
try:
syncdb()
except sqlite3.DatabaseError as err:
raise RuntimeError(
"Failed to open database file in %s. Try renaming this file and relaunch Lutris" %
settings.PGA_DB
) from err
for service in DEFAULT_SERVICES:
if not settings.read_setting(service, section="services"):
settings.write_setting(service, True, section="services")
run_all_checks()
¶
Run all startup checks
Source code in lutris/startup.py
def run_all_checks():
"""Run all startup checks"""
check_driver()
check_libs()
check_vulkan()
fill_missing_platforms()
update_runtime(force=False)
¶
Update runtime components
Source code in lutris/startup.py
def update_runtime(force=False):
"""Update runtime components"""
runtime_call = update_cache.get_last_call("runtime")
if force or not runtime_call or runtime_call > 3600 * 12:
runtime_updater = RuntimeUpdater()
components_to_update = runtime_updater.update()
if components_to_update:
while runtime_updater.current_updates:
time.sleep(0.3)
update_cache.write_date_to_cache("runtime")
for dll_manager_class in (DXVKManager, DXVKNVAPIManager, VKD3DManager, D3DExtrasManager, dgvoodoo2Manager):
key = dll_manager_class.__name__
key_call = update_cache.get_last_call(key)
if force or not key_call or key_call > 3600 * 6:
dll_manager = dll_manager_class()
dll_manager.upgrade()
update_cache.write_date_to_cache(key)
media_call = update_cache.get_last_call("media")
if force or not media_call or media_call > 3600 * 24:
sync_media()
update_cache.write_date_to_cache("media")
logger.info("Startup complete")
style_manager
¶
PORTAL_BUS_NAME
¶
PORTAL_OBJECT_PATH
¶
PORTAL_SETTINGS_INTERFACE
¶
ColorScheme (Enum)
¶
StyleManager (Object)
¶
Manages the color scheme of the app.
Has a single readable GObject property is_dark telling whether the app is
in dark mode, it is set to True, when either the user preference on the
preferences panel or in the a system is set to prefer dark mode.
Source code in lutris/style_manager.py
class StyleManager(GObject.Object):
"""Manages the color scheme of the app.
Has a single readable GObject property `is_dark` telling whether the app is
in dark mode, it is set to True, when either the user preference on the
preferences panel or in the a system is set to prefer dark mode.
"""
_color_scheme = ColorScheme.NO_PREFERENCE
_dbus_proxy = None
_is_config_dark = False
_is_dark = False
_is_system_dark = False
def __init__(self):
super().__init__()
self.gtksettings = Gtk.Settings.get_default()
self.is_config_dark = settings.read_setting("dark_theme", default="false").lower() == "true"
Gio.DBusProxy.new_for_bus(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
PORTAL_BUS_NAME,
PORTAL_OBJECT_PATH,
PORTAL_SETTINGS_INTERFACE,
None,
self._new_for_bus_cb,
)
def _read_portal_setting(self) -> None:
if not self._dbus_proxy:
return
variant = GLib.Variant.new_tuple(
GLib.Variant.new_string("org.freedesktop.appearance"),
GLib.Variant.new_string("color-scheme"),
)
self._dbus_proxy.call(
"Read",
variant,
Gio.DBusCallFlags.NONE,
GObject.G_MAXINT,
None,
self._call_cb,
)
def _new_for_bus_cb(self, obj, result):
try:
proxy = obj.new_for_bus_finish(result)
if proxy:
proxy.connect("g-signal", self._on_settings_changed)
self._dbus_proxy = proxy
self._read_portal_setting()
else:
raise Exception("Could not start GDBusProxy")
except Exception as err:
logger.error("Unable to start Settings portal: %s", err)
def _call_cb(self, obj, result):
try:
values = obj.call_finish(result)
if values:
value = values[0]
self.color_scheme = self._read_value(value)
else:
raise Exception("Could not read color-scheme")
except Exception:
pass
def _on_settings_changed(self, _proxy, _sender_name, signal_name, params):
if signal_name != "SettingChanged":
return
namespace, name, value = params
if namespace == "org.freedesktop.appearance" and name == "color-scheme":
self.color_scheme = self._read_value(value)
def _read_value(self, value: int) -> ColorScheme:
if value == 1:
return ColorScheme.PREFER_DARK
if value == 2:
return ColorScheme.PREFER_LIGHT
return ColorScheme.NO_PREFERENCE
@property
def is_system_dark(self) -> bool:
return self._is_system_dark
@is_system_dark.setter # type: ignore
def is_system_dark(self, is_system_dark: bool) -> None:
if self._is_system_dark == is_system_dark:
return
self._is_system_dark = is_system_dark
self._set_is_dark(self._is_config_dark or is_system_dark)
@property
def is_config_dark(self) -> bool:
return self._is_config_dark
@is_config_dark.setter # type: ignore
def is_config_dark(self, is_config_dark: bool) -> None:
if self._is_config_dark == is_config_dark:
return
self._is_config_dark = is_config_dark
self._set_is_dark(is_config_dark or self._is_system_dark)
@GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READABLE)
def is_dark(self) -> bool:
return self._is_dark
def _set_is_dark(self, is_dark: bool) -> None:
if self._is_dark == is_dark:
return
self._is_dark = is_dark
self.notify("is-dark")
self.gtksettings.set_property(
"gtk-application-prefer-dark-theme", is_dark
)
@property
def color_scheme(self) -> ColorScheme:
return self._color_scheme
@color_scheme.setter # type: ignore
def color_scheme(self, color_scheme: ColorScheme) -> None:
if self._color_scheme == color_scheme:
return
self._color_scheme = color_scheme
self.is_system_dark = self.color_scheme == ColorScheme.PREFER_DARK
color_scheme: ColorScheme
property
writable
¶
is_config_dark: bool
property
writable
¶
is_system_dark: bool
property
writable
¶
__init__(self)
special
¶
Source code in lutris/style_manager.py
def __init__(self):
super().__init__()
self.gtksettings = Gtk.Settings.get_default()
self.is_config_dark = settings.read_setting("dark_theme", default="false").lower() == "true"
Gio.DBusProxy.new_for_bus(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
PORTAL_BUS_NAME,
PORTAL_OBJECT_PATH,
PORTAL_SETTINGS_INTERFACE,
None,
self._new_for_bus_cb,
)
do_get_property(self, pspec)
¶
Source code in lutris/style_manager.py
def obj_get_property(self, pspec):
name = pspec.name.replace('-', '_')
return getattr(self, name, None)
do_set_property(self, pspec, value)
¶
Source code in lutris/style_manager.py
def obj_set_property(self, pspec, value):
name = pspec.name.replace('-', '_')
prop = getattr(cls, name, None)
if prop:
prop.fset(self, value)
sysoptions
¶
Options list for system config.
VULKAN_DATA_DIRS
¶
system_options
¶
get_gpu_vendor_cmd(nvidia_files)
¶
Run glxinfo command to get vendor based on certain conditions
Source code in lutris/sysoptions.py
def get_gpu_vendor_cmd(nvidia_files):
"""Run glxinfo command to get vendor based on certain conditions"""
glxinfocmd = "glxinfo | grep -i opengl | grep -i vendor"
if USE_DRI_PRIME == 1:
glxinfocmd = "DRI_PRIME=1 glxinfo | grep -i opengl | grep -i vendor"
elif nvidia_files == 1:
glxinfocmd = "__GLX_VENDOR_LIBRARY_NAME=nvidia glxinfo | grep -i opengl | grep -i vendor"
return glxinfocmd
get_optirun_choices()
¶
Return menu choices (label, value) for Optimus
Source code in lutris/sysoptions.py
def get_optirun_choices():
"""Return menu choices (label, value) for Optimus"""
choices = [(_("Off"), "off")]
if system.find_executable("primusrun"):
choices.append(("primusrun", "primusrun"))
if system.find_executable("optirun"):
choices.append(("optirun/virtualgl", "optirun"))
if system.find_executable("pvkrun"):
choices.append(("primus vk", "pvkrun"))
return choices
get_output_choices()
¶
Return list of outputs for drop-downs
Source code in lutris/sysoptions.py
def get_output_choices():
"""Return list of outputs for drop-downs"""
displays = DISPLAY_MANAGER.get_display_names()
output_choices = list(zip(displays, displays))
output_choices.insert(0, (_("Off"), "off"))
output_choices.insert(1, (_("Primary"), "primary"))
return output_choices
get_output_list()
¶
Return a list of output with their index. This is used to indicate to SDL 1.2 which monitor to use.
Source code in lutris/sysoptions.py
def get_output_list():
"""Return a list of output with their index.
This is used to indicate to SDL 1.2 which monitor to use.
"""
choices = [(_("Off"), "off")]
displays = DISPLAY_MANAGER.get_display_names()
for index, output in enumerate(displays):
# Display name can't be used because they might not be in the right order
# Using DISPLAYS to get the number of connected monitors
choices.append((output, str(index)))
return choices
get_resolution_choices()
¶
Return list of available resolutions as label, value tuples suitable for inclusion in drop-downs.
Source code in lutris/sysoptions.py
def get_resolution_choices():
"""Return list of available resolutions as label, value tuples
suitable for inclusion in drop-downs.
"""
resolutions = DISPLAY_MANAGER.get_resolutions()
resolution_choices = list(zip(resolutions, resolutions))
resolution_choices.insert(0, (_("Keep current"), "off"))
return resolution_choices
get_vk_icd_choices()
¶
Return available Vulkan ICD loaders
Source code in lutris/sysoptions.py
def get_vk_icd_choices():
"""Return available Vulkan ICD loaders"""
intel = []
amdradv = []
nvidia = []
amdvlk = []
choices = [(_("Auto: WARNING -- No Vulkan Loader detected!"), "")]
icd_files = defaultdict(list)
# Add loaders
for data_dir in VULKAN_DATA_DIRS:
path = os.path.join(data_dir, "icd.d", "*.json")
for loader in glob.glob(path):
icd_key = os.path.basename(loader).split(".")[0]
icd_files[icd_key].append(os.path.join(path, loader))
if "intel" in loader:
intel.append(loader)
elif "radeon" in loader:
amdradv.append(loader)
elif "nvidia" in loader:
nvidia.append(loader)
elif "amd_icd" in loader:
amdvlk.append(loader)
intel_files = ":".join(intel)
amdradv_files = ":".join(amdradv)
nvidia_files = ":".join(nvidia)
amdvlk_files = ":".join(amdvlk)
glxinfocmd = get_gpu_vendor_cmd(0)
if nvidia_files:
glxinfocmd = get_gpu_vendor_cmd(1)
with subprocess.Popen(glxinfocmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as glxvendorget:
glxvendor = glxvendorget.communicate()[0].decode("utf-8")
default_gpu = glxvendor
if "Intel" in default_gpu:
choices = [(_("Auto: Intel Open Source (MESA: ANV)"), intel_files)]
elif "AMD" in default_gpu:
choices = [(_("Auto: AMD RADV Open Source (MESA: RADV)"), amdradv_files)]
elif "NVIDIA" in default_gpu:
choices = [(_("Auto: Nvidia Proprietary"), nvidia_files)]
if intel_files:
choices.append(("Intel Open Source (MESA: ANV)", intel_files))
if amdradv_files:
choices.append(("AMD RADV Open Source (MESA: RADV)", amdradv_files))
if nvidia_files:
choices.append(("Nvidia Proprietary", nvidia_files))
if amdvlk_files:
choices.append(("AMDVLK/AMDGPU-PRO Proprietary", amdvlk_files))
return choices
with_runner_overrides(runner_slug)
¶
Return system options updated with overrides from given runner.
Source code in lutris/sysoptions.py
def with_runner_overrides(runner_slug):
"""Return system options updated with overrides from given runner."""
options = system_options
try:
runner = runners.import_runner(runner_slug)
except runners.InvalidRunner:
return options
if not getattr(runner, "system_options_override"):
runner = runner()
if runner.system_options_override:
opts_dict = OrderedDict((opt["option"], opt) for opt in options)
for option in runner.system_options_override:
key = option["option"]
if opts_dict.get(key):
opts_dict[key] = opts_dict[key].copy()
opts_dict[key].update(option)
else:
opts_dict[key] = option
options = list(opts_dict.values())
return options
util
special
¶
Misc common functions
selective_merge(base_obj, delta_obj)
¶
used by write_json
Source code in lutris/util/__init__.py
def selective_merge(base_obj, delta_obj):
""" used by write_json """
if not isinstance(base_obj, dict):
return delta_obj
common_keys = set(base_obj).intersection(delta_obj)
new_keys = set(delta_obj).difference(common_keys)
for k in common_keys:
base_obj[k] = selective_merge(base_obj[k], delta_obj[k])
for k in new_keys:
base_obj[k] = delta_obj[k]
return base_obj
audio
¶
Whatever it is we want to do with audio module
reset_pulse()
¶
Reset pulseaudio.
Source code in lutris/util/audio.py
def reset_pulse():
"""Reset pulseaudio."""
if not system.find_executable("pulseaudio"):
logger.warning("PulseAudio not installed. Nothing to do.")
return
system.execute(["pulseaudio", "--kill"])
time.sleep(1)
system.execute(["pulseaudio", "--start"])
logger.debug("PulseAudio restarted")
cookies
¶
WebkitCookieJar (MozillaCookieJar)
¶
Subclass of MozillaCookieJar for compatibility with cookies coming from Webkit2. This disables the magic_re header which is not present and adds compatibility with HttpOnly cookies (See http://bugs.python.org/issue2190)
Source code in lutris/util/cookies.py
class WebkitCookieJar(MozillaCookieJar):
"""Subclass of MozillaCookieJar for compatibility with cookies
coming from Webkit2.
This disables the magic_re header which is not present and adds
compatibility with HttpOnly cookies (See http://bugs.python.org/issue2190)
"""
def _really_load(self, f, filename, ignore_discard, ignore_expires): # pylint: disable=too-many-locals
now = time.time()
try:
while 1:
line = f.readline()
if line == "":
break
# last field may be absent, so keep any trailing tab
if line.endswith("\n"):
line = line[:-1]
sline = line.strip()
# support HttpOnly cookies (as stored by curl or old Firefox).
if sline.startswith("#HttpOnly_"):
line = sline[10:]
elif sline.startswith("#") or sline == "":
continue
domain, domain_specified, path, secure, expires, name, value, *_extra = line.split("\t")
secure = secure == "TRUE"
domain_specified = domain_specified == "TRUE"
if name == "":
# cookies.txt regards 'Set-Cookie: foo' as a cookie
# with no name, whereas http.cookiejar regards it as a
# cookie with no value.
name = value
value = None
initial_dot = domain.startswith(".")
assert domain_specified == initial_dot
discard = False
if expires == "":
expires = None
discard = True
# assume path_specified is false
c = Cookie(
0,
name,
value,
None,
False,
domain,
domain_specified,
initial_dot,
path,
False,
secure,
expires,
discard,
None,
None,
{},
)
if not ignore_discard and c.discard:
continue
if not ignore_expires and c.is_expired(now):
continue
self.set_cookie(c)
except OSError:
raise
except Exception as err:
_warn_unhandled_exception()
raise OSError("invalid Netscape format cookies file %r: %r" % (filename, line)) from err
datapath
¶
Utility to get the path of Lutris assets
get()
¶
Return the path for the resources.
Source code in lutris/util/datapath.py
def get():
"""Return the path for the resources."""
launch_path = os.path.realpath(sys.path[0])
if launch_path.startswith("/usr/local"):
data_path = "/usr/local/share/lutris"
elif launch_path.startswith("/usr"):
data_path = "/usr/share/lutris"
elif system.path_exists(os.path.normpath(os.path.join(sys.path[0], "share"))):
data_path = os.path.normpath(os.path.join(sys.path[0], "share/lutris"))
elif system.path_exists(os.path.normpath(os.path.join(launch_path, "../../share/lutris"))):
data_path = os.path.normpath(os.path.join(launch_path, "../../share/lutris"))
else:
import lutris
lutris_module = lutris.__file__
data_path = os.path.join(os.path.dirname(os.path.dirname(lutris_module)), "share/lutris")
if not system.path_exists(data_path):
raise IOError("data_path can't be found at : %s" % data_path)
return data_path
display
¶
Module to deal with various aspects of displays
DBUS_AVAILABLE
¶
DISPLAY_MANAGER
¶
GnomeDesktop
¶
LIB_GNOME_DESKTOP_AVAILABLE
¶
SCREEN_SAVER_INHIBITOR
¶
USE_DRI_PRIME
¶
DBusScreenSaverInhibitor
¶
Inhibit and uninhibit the screen saver using DBus. Requires the Inhibit() and UnInhibit() methods to be exposed over DBus.
Source code in lutris/util/display.py
class DBusScreenSaverInhibitor:
"""Inhibit and uninhibit the screen saver using DBus.
Requires the Inhibit() and UnInhibit() methods to be exposed over DBus."""
def __init__(self, name, path, interface, bus_type=Gio.BusType.SESSION):
self.proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type, Gio.DBusProxyFlags.NONE, None, name, path, interface, None)
def inhibit(self, game_name):
"""Inhibit the screen saver.
Returns a cookie that must be passed to the corresponding uninhibit() call.
If an error occurs, None is returned instead."""
try:
return self.proxy.Inhibit("(ss)", "Lutris", "Running game: %s" % game_name)
except Exception:
return None
def uninhibit(self, cookie):
"""Uninhibit the screen saver.
Takes a cookie as returned by inhibit. If cookie is None, no action is taken."""
if cookie is not None:
self.proxy.UnInhibit("(u)", cookie)
__init__(self, name, path, interface, bus_type=<enum G_BUS_TYPE_SESSION of type Gio.BusType>)
special
¶
Source code in lutris/util/display.py
def __init__(self, name, path, interface, bus_type=Gio.BusType.SESSION):
self.proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type, Gio.DBusProxyFlags.NONE, None, name, path, interface, None)
inhibit(self, game_name)
¶
Inhibit the screen saver. Returns a cookie that must be passed to the corresponding uninhibit() call. If an error occurs, None is returned instead.
Source code in lutris/util/display.py
def inhibit(self, game_name):
"""Inhibit the screen saver.
Returns a cookie that must be passed to the corresponding uninhibit() call.
If an error occurs, None is returned instead."""
try:
return self.proxy.Inhibit("(ss)", "Lutris", "Running game: %s" % game_name)
except Exception:
return None
uninhibit(self, cookie)
¶
Uninhibit the screen saver. Takes a cookie as returned by inhibit. If cookie is None, no action is taken.
Source code in lutris/util/display.py
def uninhibit(self, cookie):
"""Uninhibit the screen saver.
Takes a cookie as returned by inhibit. If cookie is None, no action is taken."""
if cookie is not None:
self.proxy.UnInhibit("(u)", cookie)
DesktopEnvironment (Enum)
¶
DisplayManager
¶
Get display and resolution using GnomeDesktop
Source code in lutris/util/display.py
class DisplayManager:
"""Get display and resolution using GnomeDesktop"""
def __init__(self):
if not LIB_GNOME_DESKTOP_AVAILABLE:
logger.warning("libgnomedesktop unavailable")
return
screen = Gdk.Screen.get_default()
if not screen:
raise NoScreenDetected
self.rr_screen = GnomeDesktop.RRScreen.new(screen)
self.rr_config = GnomeDesktop.RRConfig.new_current(self.rr_screen)
self.rr_config.load_current()
def get_display_names(self):
"""Return names of connected displays"""
return [output_info.get_display_name() for output_info in self.rr_config.get_outputs()]
def get_resolutions(self):
"""Return available resolutions"""
resolutions = ["%sx%s" % (mode.get_width(), mode.get_height()) for mode in self.rr_screen.list_modes()]
return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
def _get_primary_output(self):
"""Return the RROutput used as a primary display"""
for output in self.rr_screen.list_outputs():
if output.get_is_primary():
return output
return
def get_current_resolution(self):
"""Return the current resolution for the primary display"""
output = self._get_primary_output()
if not output:
logger.error("Failed to get a default output")
return "", ""
current_mode = output.get_current_mode()
return str(current_mode.get_width()), str(current_mode.get_height())
@staticmethod
def set_resolution(resolution):
"""Set the resolution of one or more displays.
The resolution can either be a string, which will be applied to the
primary display or a list of configurations as returned by `get_config`.
This method uses XrandR and will not work on Wayland.
"""
return change_resolution(resolution)
@staticmethod
def get_config():
"""Return the current display resolution
This method uses XrandR and will not work on wayland
The output can be fed in `set_resolution`
"""
return get_outputs()
__init__(self)
special
¶
Source code in lutris/util/display.py
def __init__(self):
if not LIB_GNOME_DESKTOP_AVAILABLE:
logger.warning("libgnomedesktop unavailable")
return
screen = Gdk.Screen.get_default()
if not screen:
raise NoScreenDetected
self.rr_screen = GnomeDesktop.RRScreen.new(screen)
self.rr_config = GnomeDesktop.RRConfig.new_current(self.rr_screen)
self.rr_config.load_current()
get_config()
staticmethod
¶
Return the current display resolution
This method uses XrandR and will not work on wayland
The output can be fed in set_resolution
Source code in lutris/util/display.py
@staticmethod
def get_config():
"""Return the current display resolution
This method uses XrandR and will not work on wayland
The output can be fed in `set_resolution`
"""
return get_outputs()
get_current_resolution(self)
¶
Return the current resolution for the primary display
Source code in lutris/util/display.py
def get_current_resolution(self):
"""Return the current resolution for the primary display"""
output = self._get_primary_output()
if not output:
logger.error("Failed to get a default output")
return "", ""
current_mode = output.get_current_mode()
return str(current_mode.get_width()), str(current_mode.get_height())
get_display_names(self)
¶
Return names of connected displays
Source code in lutris/util/display.py
def get_display_names(self):
"""Return names of connected displays"""
return [output_info.get_display_name() for output_info in self.rr_config.get_outputs()]
get_resolutions(self)
¶
Return available resolutions
Source code in lutris/util/display.py
def get_resolutions(self):
"""Return available resolutions"""
resolutions = ["%sx%s" % (mode.get_width(), mode.get_height()) for mode in self.rr_screen.list_modes()]
return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
set_resolution(resolution)
staticmethod
¶
Set the resolution of one or more displays.
The resolution can either be a string, which will be applied to the
primary display or a list of configurations as returned by get_config.
This method uses XrandR and will not work on Wayland.
Source code in lutris/util/display.py
@staticmethod
def set_resolution(resolution):
"""Set the resolution of one or more displays.
The resolution can either be a string, which will be applied to the
primary display or a list of configurations as returned by `get_config`.
This method uses XrandR and will not work on Wayland.
"""
return change_resolution(resolution)
NoScreenDetected (Exception)
¶
Raise this when unable to detect screens
Source code in lutris/util/display.py
class NoScreenDetected(Exception):
"""Raise this when unable to detect screens"""
disable_compositing()
¶
Disable compositing if not already disabled.
Source code in lutris/util/display.py
def disable_compositing():
"""Disable compositing if not already disabled."""
compositing_enabled = is_compositing_enabled()
if compositing_enabled is None:
compositing_enabled = True
if any(_COMPOSITING_DISABLED_STACK):
compositing_enabled = False
_COMPOSITING_DISABLED_STACK.append(compositing_enabled)
if not compositing_enabled:
return
_, stop_compositor = _get_compositor_commands()
if stop_compositor:
_run_command(*stop_compositor)
enable_compositing()
¶
Re-enable compositing if the corresponding call to disable_compositing disabled it.
Source code in lutris/util/display.py
def enable_compositing():
"""Re-enable compositing if the corresponding call to disable_compositing
disabled it."""
compositing_disabled = _COMPOSITING_DISABLED_STACK.pop()
if not compositing_disabled:
return
start_compositor, _ = _get_compositor_commands()
if start_compositor:
_run_command(*start_compositor)
get_default_dpi()
¶
Computes the DPI to use for the primary monitor which we pass to WINE.
Source code in lutris/util/display.py
def get_default_dpi():
"""Computes the DPI to use for the primary monitor
which we pass to WINE."""
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor()
scale = monitor.get_scale_factor()
dpi = 96 * scale
return int(dpi)
get_desktop_environment()
¶
Converts the value of the DESKTOP_SESSION environment variable to one of the constants in the DesktopEnvironment class. Returns None if DESKTOP_SESSION is empty or unset.
Source code in lutris/util/display.py
def get_desktop_environment():
"""Converts the value of the DESKTOP_SESSION environment variable
to one of the constants in the DesktopEnvironment class.
Returns None if DESKTOP_SESSION is empty or unset.
"""
desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
if not desktop_session:
return None
if desktop_session.endswith("plasma"):
return DesktopEnvironment.PLASMA
if desktop_session.endswith("mate"):
return DesktopEnvironment.MATE
if desktop_session.endswith("xfce"):
return DesktopEnvironment.XFCE
if desktop_session.endswith("deepin"):
return DesktopEnvironment.DEEPIN
return DesktopEnvironment.UNKNOWN
get_display_manager()
¶
Return the appropriate display manager instance. Defaults to Mutter if available. This is the only one to support Wayland.
Source code in lutris/util/display.py
def get_display_manager():
"""Return the appropriate display manager instance.
Defaults to Mutter if available. This is the only one to support Wayland.
"""
if DBUS_AVAILABLE:
try:
return MutterDisplayManager()
except DBusException as ex:
logger.debug("Mutter DBus service not reachable: %s", ex)
except Exception as ex: # pylint: disable=broad-except
logger.exception("Failed to instanciate MutterDisplayConfig. Please report with exception: %s", ex)
else:
logger.error("DBus is not available, lutris was not properly installed.")
if LIB_GNOME_DESKTOP_AVAILABLE:
try:
return DisplayManager()
except (GLib.Error, NoScreenDetected):
pass
return LegacyDisplayManager()
is_compositing_enabled()
¶
Checks whether compositing is currently disabled or enabled. Returns True for enabled, False for disabled, and None if unknown.
Source code in lutris/util/display.py
def is_compositing_enabled():
"""Checks whether compositing is currently disabled or enabled.
Returns True for enabled, False for disabled, and None if unknown.
"""
desktop_environment = get_desktop_environment()
if desktop_environment is DesktopEnvironment.PLASMA:
return _get_command_output(
"qdbus", "org.kde.KWin", "/Compositor", "org.kde.kwin.Compositing.active"
) == b"true\n"
if desktop_environment is DesktopEnvironment.MATE:
return _get_command_output("gsettings", "get org.mate.Marco.general", "compositing-manager") == b"true\n"
if desktop_environment is DesktopEnvironment.XFCE:
return _get_command_output(
"xfconf-query", "--channel=xfwm4", "--property=/general/use_compositing"
) == b"true\n"
if desktop_environment is DesktopEnvironment.DEEPIN:
return _get_command_output(
"dbus-send", "--session", "--dest=com.deepin.WMSwitcher", "--type=method_call",
"--print-reply=literal", "/com/deepin/WMSwitcher", "com.deepin.WMSwitcher.CurrentWM"
) == b"deepin wm\n"
return None
restore_gamma()
¶
Restores gamma to a normal level.
Source code in lutris/util/display.py
def restore_gamma():
"""Restores gamma to a normal level."""
xgamma_path = system.find_executable("xgamma")
try:
subprocess.Popen([xgamma_path, "-gamma", "1.0"]) # pylint: disable=consider-using-with
except (FileNotFoundError, TypeError):
logger.warning("xgamma is not available on your system")
except PermissionError:
logger.warning("you do not have permission to call xgamma")
dolphin
special
¶
cache_reader
¶
Reads the Dolphin game database, stored in a binary format
CACHE_REVISION
¶
DOLPHIN_GAME_CACHE_FILE
¶
DolphinCacheReader
¶
Source code in lutris/util/dolphin/cache_reader.py
class DolphinCacheReader:
header_size = 20
structure = {
'valid': 'b',
'file_path': 's',
'file_name': 's',
'file_size': 8,
'volume_size': 8,
'volume_size_is_accurate': 1,
'is_datel_disc': 1,
'is_nkit': 1,
'short_names': 'a',
'long_names': 'a',
'short_makers': 'a',
'long_makers': 'a',
'descriptions': 'a',
'internal_name': 's',
'game_id': 's',
'gametdb_id': 's',
'title_id': 8,
'maker_id': 's',
'region': 4,
'country': 4,
'platform': 1,
'platform_': 3,
'blob_type': 4,
'block_size': 8,
'compression_method': 's',
'revision': 2,
'disc_number': 1,
'apploader_date': 's',
'custom_name': 's',
'custom_description': 's',
'custom_maker': 's',
'volume_banner': 'i',
'custom_banner': 'i',
'default_cover': 'c',
'custom_cover': 'c',
}
def __init__(self):
self.offset = 0
with open(DOLPHIN_GAME_CACHE_FILE, "rb") as dolphin_cache_file:
self.cache_content = dolphin_cache_file.read()
if get_word_len(self.cache_content[:4]) != CACHE_REVISION:
raise Exception('Incompatible Dolphin version')
def get_game(self):
game = {}
for key, i in self.structure.items():
if i == 's':
game[key] = self.get_string()
elif i == 'b':
game[key] = self.get_boolean()
elif i == 'a':
game[key] = self.get_array()
elif i == 'i':
game[key] = self.get_image()
elif i == 'c':
game[key] = self.get_cover()
else:
game[key] = self.get_raw(i)
return game
def get_games(self):
self.offset += self.header_size
games = []
while self.offset < len(self.cache_content):
try:
games.append(self.get_game())
except Exception as ex:
logger.error("Failed to read Dolphin database: %s", ex)
return games
def get_boolean(self):
res = bool(get_word_len(self.cache_content[self.offset:self.offset + 1]))
self.offset += 1
return res
def get_array(self):
array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
array = {}
for _i in range(array_len):
array_key = self.get_raw(4)
array[array_key] = self.get_string()
return array
def get_image(self):
data_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
res = self.cache_content[self.offset:self.offset + data_len * 4] # vector<u32>
self.offset += data_len * 4
width = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
height = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
return (width, height), res
def get_cover(self):
array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
return self.get_raw(array_len)
def get_raw(self, word_len):
res = get_hex_string(self.cache_content[self.offset:self.offset + word_len])
self.offset += word_len
return res
def get_string(self):
word_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
string = self.cache_content[self.offset:self.offset + word_len]
self.offset += word_len
return string.decode('utf8')
header_size
¶
structure
¶
__init__(self)
special
¶
Source code in lutris/util/dolphin/cache_reader.py
def __init__(self):
self.offset = 0
with open(DOLPHIN_GAME_CACHE_FILE, "rb") as dolphin_cache_file:
self.cache_content = dolphin_cache_file.read()
if get_word_len(self.cache_content[:4]) != CACHE_REVISION:
raise Exception('Incompatible Dolphin version')
get_array(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_array(self):
array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
array = {}
for _i in range(array_len):
array_key = self.get_raw(4)
array[array_key] = self.get_string()
return array
get_boolean(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_boolean(self):
res = bool(get_word_len(self.cache_content[self.offset:self.offset + 1]))
self.offset += 1
return res
get_cover(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_cover(self):
array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
return self.get_raw(array_len)
get_game(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_game(self):
game = {}
for key, i in self.structure.items():
if i == 's':
game[key] = self.get_string()
elif i == 'b':
game[key] = self.get_boolean()
elif i == 'a':
game[key] = self.get_array()
elif i == 'i':
game[key] = self.get_image()
elif i == 'c':
game[key] = self.get_cover()
else:
game[key] = self.get_raw(i)
return game
get_games(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_games(self):
self.offset += self.header_size
games = []
while self.offset < len(self.cache_content):
try:
games.append(self.get_game())
except Exception as ex:
logger.error("Failed to read Dolphin database: %s", ex)
return games
get_image(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_image(self):
data_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
res = self.cache_content[self.offset:self.offset + data_len * 4] # vector<u32>
self.offset += data_len * 4
width = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
height = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
return (width, height), res
get_raw(self, word_len)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_raw(self, word_len):
res = get_hex_string(self.cache_content[self.offset:self.offset + word_len])
self.offset += word_len
return res
get_string(self)
¶
Source code in lutris/util/dolphin/cache_reader.py
def get_string(self):
word_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
self.offset += 4
string = self.cache_content[self.offset:self.offset + word_len]
self.offset += word_len
return string.decode('utf8')
get_hex_string(string)
¶
Return the hexadecimal representation of a string
Source code in lutris/util/dolphin/cache_reader.py
def get_hex_string(string):
"""Return the hexadecimal representation of a string"""
return " ".join("{:02x}".format(c) for c in string)
get_word_len(string)
¶
Return the length of a string as specified in the Dolphin format
Source code in lutris/util/dolphin/cache_reader.py
def get_word_len(string):
"""Return the length of a string as specified in the Dolphin format"""
return int("0x" + "".join("{:02x}".format(c) for c in string[::-1]), 0)
downloader
¶
get_time
¶
Downloader
¶
Non-blocking downloader.
Do start() then check_progress() at regular intervals. Download is done when check_progress() returns 1.0. Stop with cancel().
Source code in lutris/util/downloader.py
class Downloader:
"""Non-blocking downloader.
Do start() then check_progress() at regular intervals.
Download is done when check_progress() returns 1.0.
Stop with cancel().
"""
(
INIT,
DOWNLOADING,
CANCELLED,
ERROR,
COMPLETED
) = list(range(5))
def __init__(self, url, dest, overwrite=False, referer=None, callback=None):
self.url = url
self.dest = dest
self.overwrite = overwrite
self.referer = referer
self.stop_request = None
self.thread = None
self.callback = callback
# Read these after a check_progress()
self.state = self.INIT
self.error = None
self.downloaded_size = 0 # Bytes
self.full_size = 0 # Bytes
self.progress_fraction = 0
self.progress_percentage = 0
self.speed = 0
self.average_speed = 0
self.time_left = "00:00:00" # Based on average speed
self.last_size = 0
self.last_check_time = 0
self.last_speeds = []
self.speed_check_time = 0
self.time_left_check_time = 0
self.file_pointer = None
def __str__(self):
return "downloader for %s" % self.url
def start(self):
"""Start download job."""
logger.debug("⬇ %s", self.url)
self.state = self.DOWNLOADING
self.last_check_time = get_time()
if self.overwrite and os.path.isfile(self.dest):
os.remove(self.dest)
self.file_pointer = open(self.dest, "wb") # pylint: disable=consider-using-with
self.thread = jobs.AsyncCall(self.async_download, self.download_cb)
self.stop_request = self.thread.stop_request
def reset(self):
"""Reset the state of the downloader"""
self.state = self.INIT
self.error = None
self.downloaded_size = 0 # Bytes
self.full_size = 0 # Bytes
self.progress_fraction = 0
self.progress_percentage = 0
self.speed = 0
self.average_speed = 0
self.time_left = "00:00:00" # Based on average speed
self.last_size = 0
self.last_check_time = 0
self.last_speeds = []
self.speed_check_time = 0
self.time_left_check_time = 0
self.file_pointer = None
def check_progress(self):
"""Append last downloaded chunk to dest file and store stats.
:return: progress (between 0.0 and 1.0)"""
if self.state not in [self.CANCELLED, self.ERROR]:
self.get_stats()
return self.progress_fraction
def cancel(self):
"""Request download stop and remove destination file."""
logger.debug("❌ %s", self.url)
self.state = self.CANCELLED
if self.stop_request:
self.stop_request.set()
if self.file_pointer:
self.file_pointer.close()
self.file_pointer = None
if os.path.isfile(self.dest):
os.remove(self.dest)
def download_cb(self, _result, error):
if error:
logger.error("Download failed: %s", error)
self.state = self.ERROR
self.error = error
if self.file_pointer:
self.file_pointer.close()
self.file_pointer = None
return
if self.state == self.CANCELLED:
return
logger.debug("Finished downloading %s", self.url)
if not self.downloaded_size:
logger.warning("Downloaded file is empty")
if not self.full_size:
self.progress_fraction = 1.0
self.progress_percentage = 100
self.state = self.COMPLETED
self.file_pointer.close()
self.file_pointer = None
if self.callback:
self.callback()
def async_download(self, stop_request=None):
headers = requests.utils.default_headers()
headers["User-Agent"] = "Lutris/%s" % __version__
if self.referer:
headers["Referer"] = self.referer
response = requests.get(self.url, headers=headers, stream=True)
if response.status_code != 200:
logger.info("%s returned a %s error", self.url, response.status_code)
response.raise_for_status()
self.full_size = int(response.headers.get("Content-Length", "").strip() or 0)
for chunk in response.iter_content(chunk_size=1024):
if not self.file_pointer:
break
if chunk:
self.downloaded_size += len(chunk)
self.file_pointer.write(chunk)
def get_stats(self):
"""Calculate and store download stats."""
self.speed, self.average_speed = self.get_speed()
self.time_left = self.get_average_time_left()
self.last_check_time = get_time()
self.last_size = self.downloaded_size
if self.full_size:
self.progress_fraction = float(self.downloaded_size) / float(self.full_size)
self.progress_percentage = self.progress_fraction * 100
def get_speed(self):
"""Return (speed, average speed) tuple."""
elapsed_time = get_time() - self.last_check_time
chunk_size = self.downloaded_size - self.last_size
speed = chunk_size / elapsed_time or 1
self.last_speeds.append(speed)
# Average speed
if get_time() - self.speed_check_time < 1: # Minimum delay
return self.speed, self.average_speed
while len(self.last_speeds) > 20:
self.last_speeds.pop(0)
if len(self.last_speeds) > 7:
# Skim extreme values
samples = self.last_speeds[1:-1]
else:
samples = self.last_speeds[:]
average_speed = sum(samples) / len(samples)
self.speed_check_time = get_time()
return speed, average_speed
def get_average_time_left(self):
"""Return average download time left as string."""
if not self.full_size:
return "???"
elapsed_time = get_time() - self.time_left_check_time
if elapsed_time < 1: # Minimum delay
return self.time_left
average_time_left = (self.full_size - self.downloaded_size) / self.average_speed
minutes, seconds = divmod(average_time_left, 60)
hours, minutes = divmod(minutes, 60)
self.time_left_check_time = get_time()
return "%d:%02d:%02d" % (hours, minutes, seconds)
CANCELLED
¶
COMPLETED
¶
DOWNLOADING
¶
ERROR
¶
INIT
¶
__init__(self, url, dest, overwrite=False, referer=None, callback=None)
special
¶
Source code in lutris/util/downloader.py
def __init__(self, url, dest, overwrite=False, referer=None, callback=None):
self.url = url
self.dest = dest
self.overwrite = overwrite
self.referer = referer
self.stop_request = None
self.thread = None
self.callback = callback
# Read these after a check_progress()
self.state = self.INIT
self.error = None
self.downloaded_size = 0 # Bytes
self.full_size = 0 # Bytes
self.progress_fraction = 0
self.progress_percentage = 0
self.speed = 0
self.average_speed = 0
self.time_left = "00:00:00" # Based on average speed
self.last_size = 0
self.last_check_time = 0
self.last_speeds = []
self.speed_check_time = 0
self.time_left_check_time = 0
self.file_pointer = None
__str__(self)
special
¶
Source code in lutris/util/downloader.py
def __str__(self):
return "downloader for %s" % self.url
async_download(self, stop_request=None)
¶
Source code in lutris/util/downloader.py
def async_download(self, stop_request=None):
headers = requests.utils.default_headers()
headers["User-Agent"] = "Lutris/%s" % __version__
if self.referer:
headers["Referer"] = self.referer
response = requests.get(self.url, headers=headers, stream=True)
if response.status_code != 200:
logger.info("%s returned a %s error", self.url, response.status_code)
response.raise_for_status()
self.full_size = int(response.headers.get("Content-Length", "").strip() or 0)
for chunk in response.iter_content(chunk_size=1024):
if not self.file_pointer:
break
if chunk:
self.downloaded_size += len(chunk)
self.file_pointer.write(chunk)
cancel(self)
¶
Request download stop and remove destination file.
Source code in lutris/util/downloader.py
def cancel(self):
"""Request download stop and remove destination file."""
logger.debug("❌ %s", self.url)
self.state = self.CANCELLED
if self.stop_request:
self.stop_request.set()
if self.file_pointer:
self.file_pointer.close()
self.file_pointer = None
if os.path.isfile(self.dest):
os.remove(self.dest)
check_progress(self)
¶
Append last downloaded chunk to dest file and store stats.
:return: progress (between 0.0 and 1.0)
Source code in lutris/util/downloader.py
def check_progress(self):
"""Append last downloaded chunk to dest file and store stats.
:return: progress (between 0.0 and 1.0)"""
if self.state not in [self.CANCELLED, self.ERROR]:
self.get_stats()
return self.progress_fraction
download_cb(self, _result, error)
¶
Source code in lutris/util/downloader.py
def download_cb(self, _result, error):
if error:
logger.error("Download failed: %s", error)
self.state = self.ERROR
self.error = error
if self.file_pointer:
self.file_pointer.close()
self.file_pointer = None
return
if self.state == self.CANCELLED:
return
logger.debug("Finished downloading %s", self.url)
if not self.downloaded_size:
logger.warning("Downloaded file is empty")
if not self.full_size:
self.progress_fraction = 1.0
self.progress_percentage = 100
self.state = self.COMPLETED
self.file_pointer.close()
self.file_pointer = None
if self.callback:
self.callback()
get_average_time_left(self)
¶
Return average download time left as string.
Source code in lutris/util/downloader.py
def get_average_time_left(self):
"""Return average download time left as string."""
if not self.full_size:
return "???"
elapsed_time = get_time() - self.time_left_check_time
if elapsed_time < 1: # Minimum delay
return self.time_left
average_time_left = (self.full_size - self.downloaded_size) / self.average_speed
minutes, seconds = divmod(average_time_left, 60)
hours, minutes = divmod(minutes, 60)
self.time_left_check_time = get_time()
return "%d:%02d:%02d" % (hours, minutes, seconds)
get_speed(self)
¶
Return (speed, average speed) tuple.
Source code in lutris/util/downloader.py
def get_speed(self):
"""Return (speed, average speed) tuple."""
elapsed_time = get_time() - self.last_check_time
chunk_size = self.downloaded_size - self.last_size
speed = chunk_size / elapsed_time or 1
self.last_speeds.append(speed)
# Average speed
if get_time() - self.speed_check_time < 1: # Minimum delay
return self.speed, self.average_speed
while len(self.last_speeds) > 20:
self.last_speeds.pop(0)
if len(self.last_speeds) > 7:
# Skim extreme values
samples = self.last_speeds[1:-1]
else:
samples = self.last_speeds[:]
average_speed = sum(samples) / len(samples)
self.speed_check_time = get_time()
return speed, average_speed
get_stats(self)
¶
Calculate and store download stats.
Source code in lutris/util/downloader.py
def get_stats(self):
"""Calculate and store download stats."""
self.speed, self.average_speed = self.get_speed()
self.time_left = self.get_average_time_left()
self.last_check_time = get_time()
self.last_size = self.downloaded_size
if self.full_size:
self.progress_fraction = float(self.downloaded_size) / float(self.full_size)
self.progress_percentage = self.progress_fraction * 100
reset(self)
¶
Reset the state of the downloader
Source code in lutris/util/downloader.py
def reset(self):
"""Reset the state of the downloader"""
self.state = self.INIT
self.error = None
self.downloaded_size = 0 # Bytes
self.full_size = 0 # Bytes
self.progress_fraction = 0
self.progress_percentage = 0
self.speed = 0
self.average_speed = 0
self.time_left = "00:00:00" # Based on average speed
self.last_size = 0
self.last_check_time = 0
self.last_speeds = []
self.speed_check_time = 0
self.time_left_check_time = 0
self.file_pointer = None
start(self)
¶
Start download job.
Source code in lutris/util/downloader.py
def start(self):
"""Start download job."""
logger.debug("⬇ %s", self.url)
self.state = self.DOWNLOADING
self.last_check_time = get_time()
if self.overwrite and os.path.isfile(self.dest):
os.remove(self.dest)
self.file_pointer = open(self.dest, "wb") # pylint: disable=consider-using-with
self.thread = jobs.AsyncCall(self.async_download, self.download_cb)
self.stop_request = self.thread.stop_request
egs
special
¶
egs_launcher
¶
Interact with an exiting EGS install
EGSLauncher
¶
Source code in lutris/util/egs/egs_launcher.py
class EGSLauncher:
manifests_paths = 'ProgramData/Epic/EpicGamesLauncher/Data/Manifests'
def __init__(self, prefix_path):
self.prefix_path = prefix_path
def iter_manifests(self):
manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
if not os.path.exists(manifests_path):
logger.warning("No valid path for EGS games manifests in %s", manifests_path)
return []
for manifest in os.listdir(manifests_path):
if not manifest.endswith(".item"):
continue
with open(os.path.join(manifests_path, manifest), encoding='utf-8') as manifest_file:
manifest_content = json.loads(manifest_file.read())
if manifest_content["MainGameAppName"] != manifest_content["AppName"]:
continue
yield manifest_content
manifests_paths
¶
__init__(self, prefix_path)
special
¶
Source code in lutris/util/egs/egs_launcher.py
def __init__(self, prefix_path):
self.prefix_path = prefix_path
iter_manifests(self)
¶
Source code in lutris/util/egs/egs_launcher.py
def iter_manifests(self):
manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
if not os.path.exists(manifests_path):
logger.warning("No valid path for EGS games manifests in %s", manifests_path)
return []
for manifest in os.listdir(manifests_path):
if not manifest.endswith(".item"):
continue
with open(os.path.join(manifests_path, manifest), encoding='utf-8') as manifest_file:
manifest_content = json.loads(manifest_file.read())
if manifest_content["MainGameAppName"] != manifest_content["AppName"]:
continue
yield manifest_content
extract
¶
ExtractFailure (Exception)
¶
Exception raised when and archive fails to extract
Source code in lutris/util/extract.py
class ExtractFailure(Exception):
"""Exception raised when and archive fails to extract"""
check_inno_exe(path)
¶
Check if a path in a compatible innosetup archive
Source code in lutris/util/extract.py
def check_inno_exe(path):
"""Check if a path in a compatible innosetup archive"""
_innoextract_path = get_innoextract_path()
if not _innoextract_path:
logger.warning("Innoextract not found, can't determine type of archive %s", path)
return False
command = [_innoextract_path, "-i", path]
return_code = subprocess.call(command)
return return_code == 0
decompress_gog(file_path, destination_path)
¶
Source code in lutris/util/extract.py
def decompress_gog(file_path, destination_path):
innoextract_path = get_innoextract_path()
if not innoextract_path:
raise OSError("innoextract is not found in the lutris runtime or on the system")
system.create_folder(destination_path) # innoextract cannot do mkdir -p
return_code = subprocess.call([innoextract_path, "-m", "-g", "-d", destination_path, "-e", file_path])
if return_code != 0:
raise RuntimeError("innoextract failed to extract GOG setup file")
decompress_gz(file_path, dest_path)
¶
Decompress a gzip file.
Source code in lutris/util/extract.py
def decompress_gz(file_path, dest_path):
"""Decompress a gzip file."""
if dest_path:
dest_filename = os.path.join(dest_path, os.path.basename(file_path[:-3]))
else:
dest_filename = file_path[:-3]
os.makedirs(os.path.dirname(dest_filename), exist_ok=True)
with open(dest_filename, "wb") as dest_file:
gzipped_file = gzip.open(file_path, "rb")
dest_file.write(gzipped_file.read())
gzipped_file.close()
return dest_path
extract_7zip(path, dest, archive_type=None)
¶
Source code in lutris/util/extract.py
def extract_7zip(path, dest, archive_type=None):
_7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7z")
if not system.path_exists(_7zip_path):
_7zip_path = system.find_executable("7z")
if not system.path_exists(_7zip_path):
raise OSError("7zip is not found in the lutris runtime or on the system")
command = [_7zip_path, "x", path, "-o{}".format(dest), "-aoa"]
if archive_type and archive_type != "auto":
command.append("-t{}".format(archive_type))
subprocess.call(command)
extract_archive(path, to_directory='.', merge_single=True, extractor=None)
¶
Source code in lutris/util/extract.py
def extract_archive(path, to_directory=".", merge_single=True, extractor=None):
path = os.path.abspath(path)
logger.debug("Extracting %s to %s", path, to_directory)
if extractor is None:
extractor = guess_extractor(path)
opener, mode = get_archive_opener(extractor)
temp_path = temp_dir = os.path.join(to_directory, ".extract-%s" % random_id())
try:
_do_extract(path, temp_path, opener, mode, extractor)
except (OSError, zlib.error, tarfile.ReadError, EOFError) as ex:
logger.error("Extraction failed: %s", ex)
raise ExtractFailure(str(ex)) from ex
if merge_single:
extracted = os.listdir(temp_path)
if len(extracted) == 1:
temp_path = os.path.join(temp_path, extracted[0])
if os.path.isfile(temp_path):
destination_path = os.path.join(to_directory, extracted[0])
if os.path.isfile(destination_path):
logger.warning("Overwrite existing file %s", destination_path)
os.remove(destination_path)
if os.path.isdir(destination_path):
os.rename(destination_path, destination_path + random_id())
shutil.move(temp_path, to_directory)
os.removedirs(temp_dir)
else:
for archive_file in os.listdir(temp_path):
source_path = os.path.join(temp_path, archive_file)
destination_path = os.path.join(to_directory, archive_file)
# logger.debug("Moving extracted files from %s to %s", source_path, destination_path)
if system.path_exists(destination_path):
logger.warning("Overwrite existing path %s", destination_path)
if os.path.isfile(destination_path):
os.remove(destination_path)
shutil.move(source_path, destination_path)
elif os.path.isdir(destination_path):
try:
system.merge_folders(source_path, destination_path)
except OSError as ex:
logger.error(
"Failed to merge to destination %s: %s",
destination_path,
ex,
)
raise ExtractFailure(str(ex)) from ex
else:
shutil.move(source_path, destination_path)
system.remove_folder(temp_dir)
logger.debug("Finished extracting %s to %s", path, to_directory)
return path, to_directory
extract_deb(archive, dest)
¶
Extract the contents of a deb file to a destination folder
Source code in lutris/util/extract.py
def extract_deb(archive, dest):
"""Extract the contents of a deb file to a destination folder"""
extract_7zip(archive, dest, archive_type="ar")
debian_folder = os.path.join(dest, "debian")
os.makedirs(debian_folder)
control_file_exts = [".gz", ".xz", ".zst", ""]
for extension in control_file_exts:
control_tar_path = os.path.join(dest, "control.tar{}".format(extension))
if os.path.exists(control_tar_path):
shutil.move(control_tar_path, debian_folder)
break
data_file_exts = [".gz", ".xz", ".zst", ".bz2", ".lzma", ""]
for extension in data_file_exts:
data_tar_path = os.path.join(dest, "data.tar{}".format(extension))
if os.path.exists(data_tar_path):
extract_archive(data_tar_path, dest)
os.remove(data_tar_path)
break
extract_exe(path, dest)
¶
Source code in lutris/util/extract.py
def extract_exe(path, dest):
if check_inno_exe(path):
decompress_gog(path, dest)
else:
# use 7za to check if exe is an archive
_7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7za")
if not system.path_exists(_7zip_path):
_7zip_path = system.find_executable("7za")
if not system.path_exists(_7zip_path):
raise OSError("7zip is not found in the lutris runtime or on the system")
command = [_7zip_path, "t", path]
return_code = subprocess.call(command)
if return_code == 0:
extract_7zip(path, dest)
else:
raise RuntimeError("specified exe is not an archive or GOG setup file")
extract_gog(path, dest)
¶
Source code in lutris/util/extract.py
def extract_gog(path, dest):
if check_inno_exe(path):
decompress_gog(path, dest)
else:
raise RuntimeError("specified exe is not a GOG setup file")
get_archive_opener(extractor)
¶
Return the archive opener and optional mode for an extractor
Source code in lutris/util/extract.py
def get_archive_opener(extractor):
"""Return the archive opener and optional mode for an extractor"""
mode = None
if extractor == "tar":
opener, mode = tarfile.open, "r:"
elif extractor == "tgz":
opener, mode = tarfile.open, "r:gz"
elif extractor == "txz":
opener, mode = tarfile.open, "r:xz"
elif extractor == "tbz2":
opener, mode = tarfile.open, "r:bz2"
elif extractor == "tzst":
opener, mode = tarfile.open, "r:zst" # Note: not supported by tarfile yet
elif extractor == "gzip":
opener = "gz"
elif extractor == "gog":
opener = "innoextract"
elif extractor == "exe":
opener = "exe"
elif extractor == "deb":
opener = "deb"
else:
opener = "7zip"
return opener, mode
get_innoextract_list(file_path)
¶
Return the list of files contained in a GOG archive
Source code in lutris/util/extract.py
def get_innoextract_list(file_path):
"""Return the list of files contained in a GOG archive"""
output = system.read_process_output([get_innoextract_path(), "-lmq", file_path])
return [line[3:] for line in output.split("\n") if line]
get_innoextract_path()
¶
Return the path where innoextract is installed
Source code in lutris/util/extract.py
def get_innoextract_path():
"""Return the path where innoextract is installed"""
inno_dirs = [path for path in os.listdir(settings.RUNTIME_DIR) if path.startswith("innoextract")]
if inno_dirs:
inno_path = os.path.join(settings.RUNTIME_DIR, inno_dirs[0], "innoextract")
else:
inno_path = system.find_executable("innoextract")
if inno_path:
logger.warning("innoextract not available in the runtime folder, using some random version")
if system.path_exists(inno_path):
return inno_path
guess_extractor(path)
¶
Guess what extractor should be used from a file name
Source code in lutris/util/extract.py
def guess_extractor(path):
"""Guess what extractor should be used from a file name"""
if path.endswith(".tar"):
extractor = "tar"
elif path.endswith((".tar.gz", ".tgz")):
extractor = "tgz"
elif path.endswith((".tar.xz", ".txz", ".tar.lzma")):
extractor = "txz"
elif path.endswith((".tar.bz2", ".tbz2", ".tbz")):
extractor = "tbz2"
elif path.endswith((".tar.zst", ".tzst")):
extractor = "tzst"
elif path.endswith(".gz"):
extractor = "gzip"
elif path.endswith(".exe"):
extractor = "exe"
elif path.endswith(".deb"):
extractor = "deb"
else:
extractor = None
return extractor
is_7zip_supported(path, extractor)
¶
Source code in lutris/util/extract.py
def is_7zip_supported(path, extractor):
supported_extractors = (
"7z",
"xz",
"bzip2",
"gzip",
"tar",
"zip",
"ar",
"arj",
"cab",
"chm",
"cpio",
"cramfs",
"dmg",
"ext",
"fat",
"gpt",
"hfs",
"ihex",
"iso",
"lzh",
"lzma",
"mbr",
"msi",
"nsis",
"ntfs",
"qcow2",
"rar",
"rpm",
"squashfs",
"udf",
"uefi",
"vdi",
"vhd",
"vmdk",
"wim",
"xar",
"z",
"auto",
)
if extractor:
return extractor.lower() in supported_extractors
_base, ext = os.path.splitext(path)
if ext:
ext = ext.lstrip(".").lower()
return ext in supported_extractors
random_id()
¶
Return a random ID
Source code in lutris/util/extract.py
def random_id():
"""Return a random ID"""
return str(uuid.uuid4())[:8]
fileio
¶
EvilConfigParser (RawConfigParser)
¶
ConfigParser with support for evil INIs using duplicate keys.
Source code in lutris/util/fileio.py
class EvilConfigParser(RawConfigParser): # pylint: disable=too-many-ancestors
"""ConfigParser with support for evil INIs using duplicate keys."""
_SECT_TMPL = r"""
\[ # [
(?P<header>[^]]+) # very permissive!
\] # ]
"""
_OPT_TMPL = r"""
(?P<option>.*?) # very permissive!
\s*(?P<vi>{delim})\s* # any number of space/tab,
# followed by any of the
# allowed delimiters,
# followed by any space/tab
(?P<value>.*)$ # everything up to eol
"""
_OPT_NV_TMPL = r"""
(?P<option>.*?) # very permissive!
\s*(?: # any number of space/tab,
(?P<vi>{delim})\s* # optionally followed by
# any of the allowed
# delimiters, followed by any
# space/tab
(?P<value>.*))?$ # everything up to eol
"""
# Remove colon from separators since it will mess with some config files
OPTCRE = re.compile(_OPT_TMPL.format(delim="="), re.VERBOSE)
OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim="="), re.VERBOSE)
def write(self, fp, space_around_delimiters=True):
for section in self._sections:
fp.write("[{}]\n".format(section).encode("utf-8"))
for (key, value) in list(self._sections[section].items()):
if key == "__name__":
continue
if (value is not None) or (self._optcre == self.OPTCRE):
# Duplicated keys writing support inside
key = "=".join((key, str(value).replace("\n", "\n%s=" % key)))
fp.write("{}\n".format(key).encode("utf-8"))
fp.write("\n".encode("utf-8"))
OPTCRE
¶
OPTCRE_NV
¶
write(self, fp, space_around_delimiters=True)
¶
Write an .ini-format representation of the configuration state.
If `space_around_delimiters' is True (the default), delimiters between keys and values are surrounded by spaces.
Please note that comments in the original configuration file are not preserved when writing the configuration back.
Source code in lutris/util/fileio.py
def write(self, fp, space_around_delimiters=True):
for section in self._sections:
fp.write("[{}]\n".format(section).encode("utf-8"))
for (key, value) in list(self._sections[section].items()):
if key == "__name__":
continue
if (value is not None) or (self._optcre == self.OPTCRE):
# Duplicated keys writing support inside
key = "=".join((key, str(value).replace("\n", "\n%s=" % key)))
fp.write("{}\n".format(key).encode("utf-8"))
fp.write("\n".encode("utf-8"))
MultiOrderedDict (OrderedDict)
¶
dict_type to use with an EvilConfigParser instance.
Source code in lutris/util/fileio.py
class MultiOrderedDict(OrderedDict):
"""dict_type to use with an EvilConfigParser instance."""
def __setitem__(self, key, value):
if isinstance(value, list) and key in self:
self[key].extend(value)
else:
super().__setitem__(key, value)
__setitem__(self, key, value)
special
¶
Source code in lutris/util/fileio.py
def __setitem__(self, key, value):
if isinstance(value, list) and key in self:
self[key].extend(value)
else:
super().__setitem__(key, value)
game_finder
¶
Automatically detects game executables in a folder
find_linux_game_executable(path, make_executable=False)
¶
Looks for a binary or shell script that launches the game in a directory
Source code in lutris/util/game_finder.py
def find_linux_game_executable(path, make_executable=False):
"""Looks for a binary or shell script that launches the game in a directory"""
for base, _dirs, files in os.walk(path):
candidates = {}
for _file in files:
if is_excluded_elf(_file):
continue
abspath = os.path.join(base, _file)
file_type = magic.from_file(abspath)
if "ASCII text executable" in file_type:
candidates["shell"] = abspath
if "Bourne-Again shell script" in file_type:
candidates["bash"] = abspath
if "POSIX shell script executable" in file_type:
candidates["posix"] = abspath
if "64-bit LSB executable" in file_type:
candidates["64bit"] = abspath
if "32-bit LSB executable" in file_type:
candidates["32bit"] = abspath
if candidates:
if make_executable:
for candidate in candidates.values():
system.make_executable(candidate)
return (
candidates.get("shell")
or candidates.get("bash")
or candidates.get("posix")
or candidates.get("64bit")
or candidates.get("32bit")
)
logger.error("Couldn't find a Linux executable in %s", path)
return ""
find_windows_game_executable(path)
¶
Source code in lutris/util/game_finder.py
def find_windows_game_executable(path):
for base, _dirs, files in os.walk(path):
candidates = {}
if is_excluded_dir(base):
continue
for _file in files:
if is_excluded_exe(_file):
continue
abspath = os.path.join(base, _file)
if os.path.islink(abspath):
continue
file_type = magic.from_file(abspath)
if "MS Windows shortcut" in file_type:
candidates["link"] = abspath
elif "PE32+ executable (GUI) x86-64" in file_type:
candidates["64bit"] = abspath
elif "PE32 executable (GUI) Intel 80386" in file_type:
candidates["32bit"] = abspath
if candidates:
return (
candidates.get("link")
or candidates.get("64bit")
or candidates.get("32bit")
)
logger.error("Couldn't find a Windows executable in %s", path)
return ""
is_excluded_dir(path)
¶
Source code in lutris/util/game_finder.py
def is_excluded_dir(path):
excluded = (
"Internet Explorer",
"Windows NT",
"Common Files",
"Windows Media Player",
"windows",
"ProgramData",
"users",
"GameSpy Arcade"
)
return any(dir_name in excluded for dir_name in path.split("/"))
is_excluded_elf(filename)
¶
Source code in lutris/util/game_finder.py
def is_excluded_elf(filename):
excluded = (
"xdg-open",
"uninstall"
)
_fn = filename.lower()
return any(exclude in _fn for exclude in excluded)
is_excluded_exe(filename)
¶
Source code in lutris/util/game_finder.py
def is_excluded_exe(filename):
excluded = (
"unins000",
"uninstal",
"update",
"config.exe",
"gsarcade.exe",
"dosbox.exe",
)
_fn = filename.lower()
return any(exclude in _fn for exclude in excluded)
gamecontrollerdb
¶
ControllerMapping
¶
Source code in lutris/util/gamecontrollerdb.py
class ControllerMapping:
valid_keys = [
"platform",
"leftx",
"lefty",
"rightx",
"righty",
"a",
"b",
"back",
"dpdown",
"dpleft",
"dpright",
"dpup",
"guide",
"leftshoulder",
"leftstick",
"lefttrigger",
"rightshoulder",
"rightstick",
"righttrigger",
"start",
"x",
"y",
]
def __init__(self, guid, name, mapping):
self.guid = guid
self.name = name
self.mapping = mapping
self.keys = {}
self.parse()
def __str__(self):
return self.name
def parse(self):
key_maps = self.mapping.split(",")
for key_map in key_maps:
if not key_map:
continue
xinput_key, sdl_key = key_map.split(":")
if xinput_key not in self.valid_keys:
logger.warning("Unrecognized key %s", xinput_key)
continue
self.keys[xinput_key] = sdl_key
valid_keys
¶
__init__(self, guid, name, mapping)
special
¶
Source code in lutris/util/gamecontrollerdb.py
def __init__(self, guid, name, mapping):
self.guid = guid
self.name = name
self.mapping = mapping
self.keys = {}
self.parse()
__str__(self)
special
¶
Source code in lutris/util/gamecontrollerdb.py
def __str__(self):
return self.name
parse(self)
¶
Source code in lutris/util/gamecontrollerdb.py
def parse(self):
key_maps = self.mapping.split(",")
for key_map in key_maps:
if not key_map:
continue
xinput_key, sdl_key = key_map.split(":")
if xinput_key not in self.valid_keys:
logger.warning("Unrecognized key %s", xinput_key)
continue
self.keys[xinput_key] = sdl_key
GameControllerDB
¶
Source code in lutris/util/gamecontrollerdb.py
class GameControllerDB:
db_path = os.path.join(RUNTIME_DIR, "gamecontrollerdb/gamecontrollerdb.txt")
def __init__(self):
if not system.path_exists(self.db_path):
raise OSError("Path to gamecontrollerdb.txt not provided or invalid")
self.controllers = {}
self.parsedb()
def __str__(self):
return "GameControllerDB <%s>" % self.db_path
def __getitem__(self, value):
return self.controllers[value]
def parsedb(self):
with open(self.db_path, "r", encoding='utf-8') as db:
for line in db.readlines():
line = line.strip()
if not line or line.startswith("#"):
continue
guid, name, mapping = line.strip().split(",", 2)
self.controllers[guid] = ControllerMapping(guid, name, mapping)
db_path
¶
__getitem__(self, value)
special
¶
Source code in lutris/util/gamecontrollerdb.py
def __getitem__(self, value):
return self.controllers[value]
__init__(self)
special
¶
Source code in lutris/util/gamecontrollerdb.py
def __init__(self):
if not system.path_exists(self.db_path):
raise OSError("Path to gamecontrollerdb.txt not provided or invalid")
self.controllers = {}
self.parsedb()
__str__(self)
special
¶
Source code in lutris/util/gamecontrollerdb.py
def __str__(self):
return "GameControllerDB <%s>" % self.db_path
parsedb(self)
¶
Source code in lutris/util/gamecontrollerdb.py
def parsedb(self):
with open(self.db_path, "r", encoding='utf-8') as db:
for line in db.readlines():
line = line.strip()
if not line or line.startswith("#"):
continue
guid, name, mapping = line.strip().split(",", 2)
self.controllers[guid] = ControllerMapping(guid, name, mapping)
gog
¶
convert_gog_config_to_lutris(gog_config, gog_game_path)
¶
Source code in lutris/util/gog.py
def convert_gog_config_to_lutris(gog_config, gog_game_path):
play_tasks = gog_config["playTasks"]
lutris_config = {"launch_configs": []}
for task in play_tasks:
config = get_game_config(task, gog_game_path)
if not config:
continue
if task.get("isPrimary"):
lutris_config.update(config)
else:
lutris_config["launch_configs"].append(config)
return lutris_config
get_game_config(task, gog_game_path)
¶
Source code in lutris/util/gog.py
def get_game_config(task, gog_game_path):
config = {}
if "path" not in task:
return
exe = task["path"]
exe_abspath = system.fix_path_case(os.path.join(gog_game_path, exe))
if os.path.exists(exe_abspath):
exe = exe_abspath
else:
logger.warning("No executable found at %s", exe_abspath)
config["exe"] = exe
if task.get("workingDir"):
config["working_dir"] = task["workingDir"]
if task.get("arguments"):
config["args"] = task["arguments"]
if task.get("name"):
config["name"] = task["name"]
return config
get_gog_config(gog_game_path)
¶
Extract runtime information such as executable paths from GOG files
Source code in lutris/util/gog.py
def get_gog_config(gog_game_path):
"""Extract runtime information such as executable paths from GOG files"""
config_filename = [
fn
for fn in os.listdir(gog_game_path)
if fn.startswith("goggame") and fn.endswith(".info")
]
if not config_filename:
logger.error("No config file found in %s", gog_game_path)
return
gog_config_path = os.path.join(gog_game_path, config_filename[0])
with open(gog_config_path, encoding='utf-8') as gog_config_file:
gog_config = json.loads(gog_config_file.read())
return gog_config
get_gog_config_from_path(target_path)
¶
Return the GOG configuration for a root path
Source code in lutris/util/gog.py
def get_gog_config_from_path(target_path):
"""Return the GOG configuration for a root path"""
gog_game_path = get_gog_game_path(target_path)
if gog_game_path:
return get_gog_config(gog_game_path)
get_gog_game_path(target_path)
¶
Return the absolute path where a GOG game is installed
Source code in lutris/util/gog.py
def get_gog_game_path(target_path):
"""Return the absolute path where a GOG game is installed"""
gog_game_path = os.path.join(target_path, "drive_c/GOG Games/")
if not os.path.exists(gog_game_path):
logger.warning("No 'GOG Games' folder in %s", target_path)
return None
games = os.listdir(gog_game_path)
if len(games) > 1:
logger.warning("More than 1 game found, this is currently unsupported")
return os.path.join(gog_game_path, games[0])
graphics
special
¶
displayconfig
¶
DBus backed display management for Mutter
CRTC
¶
A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates.
Source code in lutris/util/graphics/displayconfig.py
class CRTC():
"""A CRTC (CRT controller) is a logical monitor, ie a portion of the
compositor coordinate space. It might correspond to multiple monitors, when
in clone mode, but not that it is possible to implement clone mode also by
setting different CRTCs to the same coordinates.
"""
def __init__(self, crtc_info):
self.crtc_info = crtc_info
def __repr__(self):
return "%s %s %s" % (self.id, self.geometry_str, self.current_mode)
@property
def id(self): # pylint: disable=invalid-name
"""The ID in the API of this CRTC"""
return str(self.crtc_info[0])
@property
def winsys_id(self):
"""the low-level ID of this CRTC
(which might be a XID, a KMS handle or something entirely different)"""
return self.crtc_info[1]
@property
def geometry_str(self):
"""Return a human readable representation of the geometry"""
return "%dx%d%s%d%s%d" % (
self.geometry[0],
self.geometry[1],
"" if self.geometry[2] < 0 else "+",
self.geometry[2],
"" if self.geometry[3] < 0 else "+",
self.geometry[3],
)
@property
def geometry(self):
"""The geometry of this CRTC
(might be invalid if the CRTC is not in use)
"""
return (int(self.crtc_info[2]), int(self.crtc_info[3]), int(self.crtc_info[4]), int(self.crtc_info[5]))
@property
def current_mode(self):
"""The current mode of the CRTC, or -1 if this CRTC is not used
Note: the size of the mode will always correspond to the width
and height of the CRTC"""
return int(self.crtc_info[6])
@property
def current_transform(self):
"""The current transform (espressed according to the wayland protocol)"""
return str(self.crtc_info[7])
@property
def transforms(self):
"""All possible transforms"""
return str(self.crtc_info[8])
@property
def properties(self):
"""Other high-level properties that affect this CRTC;
they are not necessarily reflected in the hardware.
No property is specified in this version of the API.
"""
return str(self.crtc_info[9])
current_mode
property
readonly
¶
The current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC
current_transform
property
readonly
¶
The current transform (espressed according to the wayland protocol)
geometry
property
readonly
¶
The geometry of this CRTC (might be invalid if the CRTC is not in use)
geometry_str
property
readonly
¶
Return a human readable representation of the geometry
id
property
readonly
¶
The ID in the API of this CRTC
properties
property
readonly
¶
Other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API.
transforms
property
readonly
¶
All possible transforms
winsys_id
property
readonly
¶
the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different)
__init__(self, crtc_info)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, crtc_info):
self.crtc_info = crtc_info
__repr__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
return "%s %s %s" % (self.id, self.geometry_str, self.current_mode)
DisplayConfig (tuple)
¶
DisplayConfig(monitors, name, position, transform, primary, scale)
__getnewargs__(self)
special
¶
Return self as a plain tuple. Used by copy and pickle.
Source code in lutris/util/graphics/displayconfig.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
__new__(_cls, monitors, name, position, transform, primary, scale)
special
staticmethod
¶
Create new instance of DisplayConfig(monitors, name, position, transform, primary, scale)
__repr__(self)
special
¶
Return a nicely formatted representation string
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
DisplayMode
¶
Representation of a screen mode (resolution, refresh rate)
Source code in lutris/util/graphics/displayconfig.py
class DisplayMode:
"""Representation of a screen mode (resolution, refresh rate)"""
def __init__(self, mode_info):
self.mode_info = mode_info
def __str__(self):
return "%sx%s@%s" % (self.width, self.height, self.frequency)
def __repr__(self):
return "<DisplayMode: %sx%s@%s>" % (self.width, self.height, self.frequency)
@property
def id(self): # pylint: disable=invalid-name
"""ID of the mode"""
return str(self.mode_info[0])
@property
def winsys_id(self):
"""the low-level ID of this mode"""
return str(self.mode_info[1])
@property
def width(self):
"""width in physical pixels"""
return int(self.mode_info[2])
@property
def height(self):
"""height in physical pixels"""
return int(self.mode_info[3])
@property
def frequency(self):
"""refresh rate"""
return str(self.mode_info[4])
@property
def flags(self):
"""mode flags as defined in xf86drmMode.h and randr.h"""
return self.mode_info[5]
flags
property
readonly
¶
mode flags as defined in xf86drmMode.h and randr.h
frequency
property
readonly
¶
refresh rate
height
property
readonly
¶
height in physical pixels
id
property
readonly
¶
ID of the mode
width
property
readonly
¶
width in physical pixels
winsys_id
property
readonly
¶
the low-level ID of this mode
__init__(self, mode_info)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, mode_info):
self.mode_info = mode_info
__repr__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
return "<DisplayMode: %sx%s@%s>" % (self.width, self.height, self.frequency)
__str__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __str__(self):
return "%sx%s@%s" % (self.width, self.height, self.frequency)
DisplayState
¶
Snapshot of a display configuration at a given time
Source code in lutris/util/graphics/displayconfig.py
class DisplayState:
"""Snapshot of a display configuration at a given time"""
def __init__(self, interface):
self.interface = interface
self._state = self.load_state()
def load_state(self):
"""Return current state from dbus interface"""
return self.interface.GetCurrentState()
@property
def serial(self):
"""Configuration serial"""
return self._state[0]
@property
def monitors(self):
"""Available monitors"""
return [Monitor(monitor) for monitor in self._state[1]]
@property
def logical_monitors(self):
"""Current logical monitor configuration"""
return [LogicalMonitor(l_m, self.monitors) for l_m in self._state[2]]
@property
def properties(self):
"""Display configuration properties"""
return self._state[3]
def get_current_mode(self):
"""Return the current mode"""
return self.monitors[0].get_current_mode()
logical_monitors
property
readonly
¶
Current logical monitor configuration
monitors
property
readonly
¶
Available monitors
properties
property
readonly
¶
Display configuration properties
serial
property
readonly
¶
Configuration serial
__init__(self, interface)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, interface):
self.interface = interface
self._state = self.load_state()
get_current_mode(self)
¶
Return the current mode
Source code in lutris/util/graphics/displayconfig.py
def get_current_mode(self):
"""Return the current mode"""
return self.monitors[0].get_current_mode()
load_state(self)
¶
Return current state from dbus interface
Source code in lutris/util/graphics/displayconfig.py
def load_state(self):
"""Return current state from dbus interface"""
return self.interface.GetCurrentState()
LogicalMonitor
¶
A logical monitor. Similar to CRTCs but logical monitors also contain scaling information.
Source code in lutris/util/graphics/displayconfig.py
class LogicalMonitor:
"""A logical monitor. Similar to CRTCs but logical monitors also contain
scaling information.
"""
def __init__(self, lm_info, monitors):
self._lm = lm_info
self._monitors = monitors
@property
def position(self):
"""Return the position of the monitor"""
return int(self._lm[0]), int(self._lm[1])
@property
def scale(self):
"""Scale"""
return self._lm[2]
@property
def transform(self):
"""Transforms
Possible transform values:
0: normal
1: 90°
2: 180°
3: 270°
4: flipped
5: 90° flipped
6: 180° flipped
7: 270° flipped
"""
return self._lm[3]
@property
def primary(self):
"""True if this is the primary logical monitor"""
return bool(self._lm[4])
def _get_monitor_for_connector(self, connector):
"""Return a Monitor instance from its connector name"""
for monitor in self._monitors:
if monitor.name == str(connector):
return monitor
return
@property
def monitors(self):
"""Monitors displaying that logical monitor"""
return [self._get_monitor_for_connector(m[0]) for m in self._lm[5]]
@property
def properties(self):
"""Possibly other properties"""
return self._lm[6]
def get_config(self):
"""Export the current configuration so it can be stored then reapplied later"""
monitors = [(monitor.name, monitor.get_current_mode().id) for monitor in self.monitors]
return DisplayConfig(monitors, self.monitors[0].name, self.position, self.transform, self.primary, self.scale)
monitors
property
readonly
¶
Monitors displaying that logical monitor
position
property
readonly
¶
Return the position of the monitor
primary
property
readonly
¶
True if this is the primary logical monitor
properties
property
readonly
¶
Possibly other properties
scale
property
readonly
¶
Scale
transform
property
readonly
¶
Transforms
Possible transform values: 0: normal 1: 90° 2: 180° 3: 270° 4: flipped 5: 90° flipped 6: 180° flipped 7: 270° flipped
__init__(self, lm_info, monitors)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, lm_info, monitors):
self._lm = lm_info
self._monitors = monitors
get_config(self)
¶
Export the current configuration so it can be stored then reapplied later
Source code in lutris/util/graphics/displayconfig.py
def get_config(self):
"""Export the current configuration so it can be stored then reapplied later"""
monitors = [(monitor.name, monitor.get_current_mode().id) for monitor in self.monitors]
return DisplayConfig(monitors, self.monitors[0].name, self.position, self.transform, self.primary, self.scale)
Monitor
¶
A physical monitor
Source code in lutris/util/graphics/displayconfig.py
class Monitor:
"""A physical monitor"""
def __init__(self, monitor):
self._monitor = monitor
def get_current_mode(self):
"""Return the current mode"""
for mode in self.get_modes():
if mode.is_current:
return mode
return
def get_modes(self):
"""Return available modes"""
return [MonitorMode(mode) for mode in self._monitor[1]]
def get_mode_for_resolution(self, resolution):
"""Return an appropriate mode for a given resolution"""
width, height = [int(i) for i in resolution.split("x")]
for mode in self.get_modes():
if mode.width == width and mode.height == height:
return mode
return
@property
def name(self):
"""Name of the connector"""
return str(self._monitor[0][0])
@property
def vendor(self):
"""Manufacturer of the monitor"""
return str(self._monitor[0][1])
@property
def model(self):
"""Model name of the monitor"""
return str(self._monitor[0][2])
@property
def serial_number(self):
"""Serial number"""
return str(self._monitor[0][3])
@property
def is_underscanning(self):
"""Return true if the monitor is underscanning"""
return bool(self._monitor[2]['is-underscanning'])
@property
def is_builtin(self):
"""Return true if the display is builtin the machine (a laptop or a tablet)"""
return bool(self._monitor[2]['is-builtin'])
@property
def display_name(self):
"""Human readable name of the display"""
return str(self._monitor[2]['display-name'])
display_name
property
readonly
¶
Human readable name of the display
is_builtin
property
readonly
¶
Return true if the display is builtin the machine (a laptop or a tablet)
is_underscanning
property
readonly
¶
Return true if the monitor is underscanning
model
property
readonly
¶
Model name of the monitor
name
property
readonly
¶
Name of the connector
serial_number
property
readonly
¶
Serial number
vendor
property
readonly
¶
Manufacturer of the monitor
__init__(self, monitor)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, monitor):
self._monitor = monitor
get_current_mode(self)
¶
Return the current mode
Source code in lutris/util/graphics/displayconfig.py
def get_current_mode(self):
"""Return the current mode"""
for mode in self.get_modes():
if mode.is_current:
return mode
return
get_mode_for_resolution(self, resolution)
¶
Return an appropriate mode for a given resolution
Source code in lutris/util/graphics/displayconfig.py
def get_mode_for_resolution(self, resolution):
"""Return an appropriate mode for a given resolution"""
width, height = [int(i) for i in resolution.split("x")]
for mode in self.get_modes():
if mode.width == width and mode.height == height:
return mode
return
get_modes(self)
¶
Return available modes
Source code in lutris/util/graphics/displayconfig.py
def get_modes(self):
"""Return available modes"""
return [MonitorMode(mode) for mode in self._monitor[1]]
MonitorMode (DisplayMode)
¶
Represents a mode given by a Monitor instance In addition to DisplayMode objects, this gives acces to the current scaling used and some additional properties like is_current.
Source code in lutris/util/graphics/displayconfig.py
class MonitorMode(DisplayMode):
"""Represents a mode given by a Monitor instance
In addition to DisplayMode objects, this gives acces to the current scaling
used and some additional properties like is_current.
"""
@property
def width(self):
"""width in physical pixels"""
return int(self.mode_info[1])
@property
def height(self):
"""height in physical pixels"""
return int(self.mode_info[2])
@property
def frequency(self):
"""refresh rate"""
return str(self.mode_info[3])
@property
def scale(self):
"""scale preferred as per calculations"""
return float(self.mode_info[4])
@property
def supported_scale(self):
"""scales supported by this mode"""
return self.mode_info[5]
@property
def properties(self):
"""Additional properties"""
return self.mode_info[6]
@property
def is_current(self):
"""Return True if the mode is the current one"""
return "is-current" in self.properties
frequency
property
readonly
¶
refresh rate
height
property
readonly
¶
height in physical pixels
is_current
property
readonly
¶
Return True if the mode is the current one
properties
property
readonly
¶
Additional properties
scale
property
readonly
¶
scale preferred as per calculations
supported_scale
property
readonly
¶
scales supported by this mode
width
property
readonly
¶
width in physical pixels
MutterDisplayConfig
¶
Class to interact with the Mutter.DisplayConfig service
Source code in lutris/util/graphics/displayconfig.py
class MutterDisplayConfig():
"""Class to interact with the Mutter.DisplayConfig service"""
namespace = "org.gnome.Mutter.DisplayConfig"
dbus_path = "/org/gnome/Mutter/DisplayConfig"
# Methods used in ApplyMonitorConfig
VERIFY_METHOD = 0
TEMPORARY_METHOD = 1
PERSISTENT_METHOD = 2
def __init__(self):
session_bus = dbus.SessionBus()
proxy_obj = session_bus.get_object(self.namespace, self.dbus_path)
self.interface = dbus.Interface(proxy_obj, dbus_interface=self.namespace)
self.resources = self.interface.GetResources()
self.current_state = DisplayState(self.interface)
@property
def serial(self):
"""
@serial is an unique identifier representing the current state
of the screen. It must be passed back to ApplyConfiguration()
and will be increased for every configuration change (so that
mutter can detect that the new configuration is based on old
state)
"""
return self.resources[0]
@property
def crtcs(self):
"""
A CRTC (CRT controller) is a logical monitor, ie a portion
of the compositor coordinate space. It might correspond
to multiple monitors, when in clone mode, but not that
it is possible to implement clone mode also by setting different
CRTCs to the same coordinates.
The number of CRTCs represent the maximum number of monitors
that can be set to expand and it is a HW constraint; if more
monitors are connected, then necessarily some will clone. This
is complementary to the concept of the encoder (not exposed in
the API), which groups outputs that necessarily will show the
same image (again a HW constraint).
A CRTC is represented by a DBus structure with the following
layout:
* u ID: the ID in the API of this CRTC
* x winsys_id: the low-level ID of this CRTC (which might
be a XID, a KMS handle or something entirely
different)
* i x, y, width, height: the geometry of this CRTC
(might be invalid if the CRTC is not in
use)
* i current_mode: the current mode of the CRTC, or -1 if this
CRTC is not used
Note: the size of the mode will always correspond
to the width and height of the CRTC
* u current_transform: the current transform (espressed according
to the wayland protocol)
* au transforms: all possible transforms
* a{sv} properties: other high-level properties that affect this
CRTC; they are not necessarily reflected in
the hardware.
No property is specified in this version of the API.
Note: all geometry information refers to the untransformed
display.
"""
return [CRTC(crtc) for crtc in self.resources[1]]
@property
def outputs(self):
"""
An output represents a physical screen, connected somewhere to
the computer. Floating connectors are not exposed in the API.
An output is a DBus struct with the following fields:
* u ID: the ID in the API
* x winsys_id: the low-level ID of this output (XID or KMS handle)
* i current_crtc: the CRTC that is currently driving this output,
or -1 if the output is disabled
* au possible_crtcs: all CRTCs that can control this output
* s name: the name of the connector to which the output is attached
(like VGA1 or HDMI)
* au modes: valid modes for this output
* au clones: valid clones for this output, ie other outputs that
can be assigned the same CRTC as this one; if you
want to mirror two outputs that don't have each other
in the clone list, you must configure two different
CRTCs for the same geometry
* a{sv} properties: other high-level properties that affect this
output; they are not necessarily reflected in
the hardware.
Known properties:
- "vendor" (s): (readonly) the human readable name
of the manufacturer
- "product" (s): (readonly) the human readable name
of the display model
- "serial" (s): (readonly) the serial number of this
particular hardware part
- "display-name" (s): (readonly) a human readable name
of this output, to be shown in the UI
- "backlight" (i): (readonly, use the specific interface)
the backlight value as a percentage
(-1 if not supported)
- "primary" (b): whether this output is primary
or not
- "presentation" (b): whether this output is
for presentation only
Note: properties might be ignored if not consistenly
applied to all outputs in the same clone group. In
general, it's expected that presentation or primary
outputs will not be cloned.
"""
return [Output(output) for output in self.resources[2]]
@property
def modes(self):
"""
A mode represents a set of parameters that are applied to
each output, such as resolution and refresh rate. It is a separate
object so that it can be referenced by CRTCs and outputs.
Multiple outputs in the same CRTCs must all have the same mode.
A mode is exposed as:
* u ID: the ID in the API
* x winsys_id: the low-level ID of this mode
* u width, height: the resolution
* d frequency: refresh rate
* u flags: mode flags as defined in xf86drmMode.h and randr.h
Output and modes are read-only objects (except for output properties),
they can change only in accordance to HW changes (such as hotplugging
a monitor), while CRTCs can be changed with ApplyConfiguration().
XXX: actually, if you insist enough, you can add new modes
through xrandr command line or the KMS API, overriding what the
kernel driver and the EDID say.
Usually, it only matters with old cards with broken drivers, or
old monitors with broken EDIDs, but it happens more often with
projectors (if for example the kernel driver doesn't add the
640x480 - 800x600 - 1024x768 default modes). Probably something
that we need to handle in mutter anyway.
"""
return [DisplayMode(mode) for mode in self.resources[3]]
@property
def max_screen_width(self):
"""Maximum width supported"""
return self.resources[4]
@property
def max_screen_height(self):
"""Maximum height supported"""
return self.resources[5]
def get_mode_for_resolution(self, resolution):
"""Return an appropriate mode for a given resolution"""
width, height = [int(i) for i in resolution.split("x")]
for mode in self.modes:
if mode.width == width and mode.height == height:
return mode
return
def get_primary_output(self):
"""Return the primary output"""
for output in self.current_state.logical_monitors:
if output.primary:
return output
return
def apply_monitors_config(self, display_configs):
"""Set the selected display to the desired resolution"""
# Reload resources
if not display_configs:
logger.error("No display config given, not applying anything")
return
self.resources = self.interface.GetResources()
self.current_state = DisplayState(self.interface)
monitors_config = [
[
config.position[0], config.position[1],
dbus.Double(config.scale),
dbus.UInt32(config.transform), config.primary,
[
[dbus.String(str(display_name)), dbus.String(str(mode)), {}]
for display_name, mode in config.monitors
]
] for config in display_configs
]
self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {})
PERSISTENT_METHOD
¶
TEMPORARY_METHOD
¶
VERIFY_METHOD
¶
crtcs
property
readonly
¶
A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates.
The number of CRTCs represent the maximum number of monitors that can be set to expand and it is a HW constraint; if more monitors are connected, then necessarily some will clone. This is complementary to the concept of the encoder (not exposed in the API), which groups outputs that necessarily will show the same image (again a HW constraint).
A CRTC is represented by a DBus structure with the following layout: * u ID: the ID in the API of this CRTC * x winsys_id: the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different) * i x, y, width, height: the geometry of this CRTC (might be invalid if the CRTC is not in use) * i current_mode: the current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC * u current_transform: the current transform (espressed according to the wayland protocol) * au transforms: all possible transforms * a{sv} properties: other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API.
Note: all geometry information refers to the untransformed display.
dbus_path
¶
max_screen_height
property
readonly
¶
Maximum height supported
max_screen_width
property
readonly
¶
Maximum width supported
modes
property
readonly
¶
A mode represents a set of parameters that are applied to each output, such as resolution and refresh rate. It is a separate object so that it can be referenced by CRTCs and outputs. Multiple outputs in the same CRTCs must all have the same mode. A mode is exposed as: * u ID: the ID in the API * x winsys_id: the low-level ID of this mode * u width, height: the resolution * d frequency: refresh rate * u flags: mode flags as defined in xf86drmMode.h and randr.h
Output and modes are read-only objects (except for output properties), they can change only in accordance to HW changes (such as hotplugging a monitor), while CRTCs can be changed with ApplyConfiguration().
XXX: actually, if you insist enough, you can add new modes through xrandr command line or the KMS API, overriding what the kernel driver and the EDID say. Usually, it only matters with old cards with broken drivers, or old monitors with broken EDIDs, but it happens more often with projectors (if for example the kernel driver doesn't add the 640x480 - 800x600 - 1024x768 default modes). Probably something that we need to handle in mutter anyway.
namespace
¶
outputs
property
readonly
¶
An output represents a physical screen, connected somewhere to the computer. Floating connectors are not exposed in the API. An output is a DBus struct with the following fields: * u ID: the ID in the API * x winsys_id: the low-level ID of this output (XID or KMS handle) * i current_crtc: the CRTC that is currently driving this output, or -1 if the output is disabled * au possible_crtcs: all CRTCs that can control this output * s name: the name of the connector to which the output is attached (like VGA1 or HDMI) * au modes: valid modes for this output * au clones: valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry * a{sv} properties: other high-level properties that affect this output; they are not necessarily reflected in the hardware. Known properties: - "vendor" (s): (readonly) the human readable name of the manufacturer - "product" (s): (readonly) the human readable name of the display model - "serial" (s): (readonly) the serial number of this particular hardware part - "display-name" (s): (readonly) a human readable name of this output, to be shown in the UI - "backlight" (i): (readonly, use the specific interface) the backlight value as a percentage (-1 if not supported) - "primary" (b): whether this output is primary or not - "presentation" (b): whether this output is for presentation only Note: properties might be ignored if not consistenly applied to all outputs in the same clone group. In general, it's expected that presentation or primary outputs will not be cloned.
serial
property
readonly
¶
@serial is an unique identifier representing the current state of the screen. It must be passed back to ApplyConfiguration() and will be increased for every configuration change (so that mutter can detect that the new configuration is based on old state)
__init__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self):
session_bus = dbus.SessionBus()
proxy_obj = session_bus.get_object(self.namespace, self.dbus_path)
self.interface = dbus.Interface(proxy_obj, dbus_interface=self.namespace)
self.resources = self.interface.GetResources()
self.current_state = DisplayState(self.interface)
apply_monitors_config(self, display_configs)
¶
Set the selected display to the desired resolution
Source code in lutris/util/graphics/displayconfig.py
def apply_monitors_config(self, display_configs):
"""Set the selected display to the desired resolution"""
# Reload resources
if not display_configs:
logger.error("No display config given, not applying anything")
return
self.resources = self.interface.GetResources()
self.current_state = DisplayState(self.interface)
monitors_config = [
[
config.position[0], config.position[1],
dbus.Double(config.scale),
dbus.UInt32(config.transform), config.primary,
[
[dbus.String(str(display_name)), dbus.String(str(mode)), {}]
for display_name, mode in config.monitors
]
] for config in display_configs
]
self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {})
get_mode_for_resolution(self, resolution)
¶
Return an appropriate mode for a given resolution
Source code in lutris/util/graphics/displayconfig.py
def get_mode_for_resolution(self, resolution):
"""Return an appropriate mode for a given resolution"""
width, height = [int(i) for i in resolution.split("x")]
for mode in self.modes:
if mode.width == width and mode.height == height:
return mode
return
get_primary_output(self)
¶
Return the primary output
Source code in lutris/util/graphics/displayconfig.py
def get_primary_output(self):
"""Return the primary output"""
for output in self.current_state.logical_monitors:
if output.primary:
return output
return
MutterDisplayManager
¶
Manage displays using the DBus Mutter interface
Source code in lutris/util/graphics/displayconfig.py
class MutterDisplayManager:
"""Manage displays using the DBus Mutter interface"""
def __init__(self):
self.display_config = MutterDisplayConfig()
def get_config(self):
"""Return the current configuration for each logical monitor"""
return [logical_monitor.get_config() for logical_monitor in self.display_config.current_state.logical_monitors]
def get_display_names(self):
"""Return display names of connected displays"""
return [output.display_name for output in self.display_config.outputs]
def get_resolutions(self):
"""Return available resolutions"""
resolutions = ["%sx%s" % (mode.width, mode.height) for mode in self.display_config.modes]
return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
def get_current_resolution(self):
"""Return the current resolution for the primary display"""
logger.debug("Retrieving current resolution")
current_mode = self.display_config.current_state.get_current_mode()
if not current_mode:
logger.error("Could not retrieve the current display mode")
return "", ""
return str(current_mode.width), str(current_mode.height)
def set_resolution(self, resolution):
"""Change the current resolution"""
if isinstance(resolution, str):
output = self.display_config.get_primary_output()
mode = output.monitors[0].get_mode_for_resolution(resolution)
if not mode:
logger.error("Could not find valid mode for %s", resolution)
return
config = [
DisplayConfig([(output.monitors[0].name, mode.id)], output.monitors[0].name, (0, 0), 0, True, 1.0)
]
self.display_config.apply_monitors_config(config)
elif resolution:
self.display_config.apply_monitors_config(resolution)
else:
return
# Load a fresh config since the current one has changed
self.display_config = MutterDisplayConfig()
__init__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self):
self.display_config = MutterDisplayConfig()
get_config(self)
¶
Return the current configuration for each logical monitor
Source code in lutris/util/graphics/displayconfig.py
def get_config(self):
"""Return the current configuration for each logical monitor"""
return [logical_monitor.get_config() for logical_monitor in self.display_config.current_state.logical_monitors]
get_current_resolution(self)
¶
Return the current resolution for the primary display
Source code in lutris/util/graphics/displayconfig.py
def get_current_resolution(self):
"""Return the current resolution for the primary display"""
logger.debug("Retrieving current resolution")
current_mode = self.display_config.current_state.get_current_mode()
if not current_mode:
logger.error("Could not retrieve the current display mode")
return "", ""
return str(current_mode.width), str(current_mode.height)
get_display_names(self)
¶
Return display names of connected displays
Source code in lutris/util/graphics/displayconfig.py
def get_display_names(self):
"""Return display names of connected displays"""
return [output.display_name for output in self.display_config.outputs]
get_resolutions(self)
¶
Return available resolutions
Source code in lutris/util/graphics/displayconfig.py
def get_resolutions(self):
"""Return available resolutions"""
resolutions = ["%sx%s" % (mode.width, mode.height) for mode in self.display_config.modes]
return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
set_resolution(self, resolution)
¶
Change the current resolution
Source code in lutris/util/graphics/displayconfig.py
def set_resolution(self, resolution):
"""Change the current resolution"""
if isinstance(resolution, str):
output = self.display_config.get_primary_output()
mode = output.monitors[0].get_mode_for_resolution(resolution)
if not mode:
logger.error("Could not find valid mode for %s", resolution)
return
config = [
DisplayConfig([(output.monitors[0].name, mode.id)], output.monitors[0].name, (0, 0), 0, True, 1.0)
]
self.display_config.apply_monitors_config(config)
elif resolution:
self.display_config.apply_monitors_config(resolution)
else:
return
# Load a fresh config since the current one has changed
self.display_config = MutterDisplayConfig()
Output
¶
Representation of a physical display output
Source code in lutris/util/graphics/displayconfig.py
class Output:
"""Representation of a physical display output"""
def __init__(self, output_info):
self._output = output_info
def __repr__(self):
return "<Output: %s %s (%s)>" % (self.vendor, self.product, self.display_name)
@property
def output_id(self):
"""ID of the output"""
return self._output[0]
@property
def winsys_id(self):
"""The low-level ID of this output (XID or KMS handle)"""
return self._output[1]
@property
def current_crtc(self):
"""The CRTC that is currently driving this output,
or -1 if the output is disabled
"""
return self._output[2]
@property
def crtcs(self):
"""All CRTCs that can control this output"""
return self._output[3]
@property
def name(self):
"""The name of the connector to which the output is attached (like VGA1 or HDMI)"""
return self._output[4]
@property
def modes(self):
"""Valid modes for this output"""
return [int(mode_id) for mode_id in self._output[5]]
@property
def clones(self):
"""Valid clones for this output, ie other outputs that can be assigned
the same CRTC as this one; if you want to mirror two outputs that don't
have each other in the clone list, you must configure two different
CRTCs for the same geometry.
"""
return self._output[6]
@property
def properties(self):
"""Other high-level properties that affect this output; they are not
necessarily reflected in the hardware.
"""
return self._output[7]
@property
def vendor(self):
"""Vendor name of the output"""
return str(self._output[7]["vendor"])
@property
def product(self):
"""Product name of the output"""
return str(self._output[7]["product"])
@property
def display_name(self):
"""A human readable name of this output, to be shown in the UI"""
return str(self._output[7]["display-name"])
@property
def is_primary(self):
"""True if the output is the primary one"""
return bool(self._output[7]["primary"])
clones
property
readonly
¶
Valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry.
crtcs
property
readonly
¶
All CRTCs that can control this output
current_crtc
property
readonly
¶
The CRTC that is currently driving this output, or -1 if the output is disabled
display_name
property
readonly
¶
A human readable name of this output, to be shown in the UI
is_primary
property
readonly
¶
True if the output is the primary one
modes
property
readonly
¶
Valid modes for this output
name
property
readonly
¶
The name of the connector to which the output is attached (like VGA1 or HDMI)
output_id
property
readonly
¶
ID of the output
product
property
readonly
¶
Product name of the output
properties
property
readonly
¶
Other high-level properties that affect this output; they are not necessarily reflected in the hardware.
vendor
property
readonly
¶
Vendor name of the output
winsys_id
property
readonly
¶
The low-level ID of this output (XID or KMS handle)
__init__(self, output_info)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, output_info):
self._output = output_info
__repr__(self)
special
¶
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
return "<Output: %s %s (%s)>" % (self.vendor, self.product, self.display_name)
drivers
¶
Hardware driver related utilities
Everything in this module should rely on /proc or /sys only, no executable calls
MIN_RECOMMENDED_NVIDIA_DRIVER
¶
check_driver()
¶
Report on the currently running driver
Source code in lutris/util/graphics/drivers.py
def check_driver():
"""Report on the currently running driver"""
if is_nvidia():
driver_info = get_nvidia_driver_info()
# pylint: disable=logging-format-interpolation
logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"]))
gpus = get_nvidia_gpu_ids()
for gpu_id in gpus:
gpu_info = get_nvidia_gpu_info(gpu_id)
logger.info("GPU: %s", gpu_info.get("Model"))
for card in get_gpus():
# pylint: disable=logging-format-interpolation
logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} using {DRIVER} driver".format(**get_gpu_info(card)))
get_gpu_info(card)
¶
Return information about a GPU
Source code in lutris/util/graphics/drivers.py
def get_gpu_info(card):
"""Return information about a GPU"""
infos = {"DRIVER": "", "PCI_ID": "", "PCI_SUBSYS_ID": ""}
try:
with open("/sys/class/drm/%s/device/uevent" % card, encoding='utf-8') as card_uevent:
content = card_uevent.readlines()
except FileNotFoundError:
logger.error("Unable to read driver information for card %s", card)
return infos
for line in content:
key, value = line.split("=", 1)
infos[key] = value.strip()
return infos
get_gpus()
¶
Return GPUs connected to the system
Source code in lutris/util/graphics/drivers.py
def get_gpus():
"""Return GPUs connected to the system"""
if not os.path.exists("/sys/class/drm"):
logger.error("No GPU available on this system!")
return
for cardname in os.listdir("/sys/class/drm/"):
if re.match(r"^card\d$", cardname):
yield cardname
get_nvidia_driver_info()
¶
Return information about NVidia drivers
Source code in lutris/util/graphics/drivers.py
def get_nvidia_driver_info():
"""Return information about NVidia drivers"""
version_file_path = "/proc/driver/nvidia/version"
if not os.path.exists(version_file_path):
return
with open(version_file_path, encoding='utf-8') as version_file:
content = version_file.readlines()
nvrm_version = content[0].split(': ')[1].strip().split()
return {
'nvrm': {
'vendor': nvrm_version[0],
'platform': nvrm_version[1],
'arch': nvrm_version[2],
'version': nvrm_version[5],
'date': ' '.join(nvrm_version[6:])
}
}
return
get_nvidia_gpu_ids()
¶
Return the list of Nvidia GPUs
Source code in lutris/util/graphics/drivers.py
def get_nvidia_gpu_ids():
"""Return the list of Nvidia GPUs"""
return os.listdir("/proc/driver/nvidia/gpus")
get_nvidia_gpu_info(gpu_id)
¶
Return details about a GPU
Source code in lutris/util/graphics/drivers.py
def get_nvidia_gpu_info(gpu_id):
"""Return details about a GPU"""
with open("/proc/driver/nvidia/gpus/%s/information" % gpu_id, encoding='utf-8') as info_file:
content = info_file.readlines()
infos = {}
for line in content:
key, value = line.split(":", 1)
infos[key] = value.strip()
return infos
is_amd()
¶
Return true if the system uses the AMD driver
Source code in lutris/util/graphics/drivers.py
def is_amd():
"""Return true if the system uses the AMD driver"""
for card in get_gpus():
if get_gpu_info(card)["DRIVER"] == "amdgpu":
return True
return False
is_nvidia()
¶
Return true if the Nvidia drivers are currently in use
Source code in lutris/util/graphics/drivers.py
def is_nvidia():
"""Return true if the Nvidia drivers are currently in use"""
return os.path.exists("/proc/driver/nvidia")
is_outdated()
¶
Source code in lutris/util/graphics/drivers.py
def is_outdated():
if not is_nvidia():
return False
driver_info = get_nvidia_driver_info()
driver_version = driver_info["nvrm"]["version"]
if not driver_version:
logger.error("Failed to get Nvidia version")
return True
major_version = int(driver_version.split(".")[0])
return major_version < MIN_RECOMMENDED_NVIDIA_DRIVER
glxinfo
¶
Parser for the glxinfo utility
Container
¶
A dummy container for data
Source code in lutris/util/graphics/glxinfo.py
class Container: # pylint: disable=too-few-public-methods
"""A dummy container for data"""
GlxInfo
¶
Give access to the glxinfo information
Source code in lutris/util/graphics/glxinfo.py
class GlxInfo:
"""Give access to the glxinfo information"""
def __init__(self, output=None):
"""Creates a new GlxInfo object
Params:
output (str): If provided, use this as the glxinfo output instead
of running the program, useful for testing.
"""
self._output = output or self.get_glxinfo_output()
self._section = None
self._attrs = set() # Keep a reference of the created attributes
self.parse()
@staticmethod
def get_glxinfo_output():
"""Return the glxinfo -B output"""
return read_process_output(["glxinfo", "-B"])
def as_dict(self):
"""Return the attributes as a dict"""
return {attr: getattr(self, attr) for attr in self._attrs}
def parse(self):
"""Converts the glxinfo output to class attributes"""
if not self._output:
logger.error("No available glxinfo output")
return
# Fix glxinfo output (Great, you saved one line by
# combining display and screen)
output = self._output.replace(" screen", "\nscreen")
for line in output.split("\n"):
if not line.strip():
continue
key, value = line.split(":", 1)
key = key.replace(" string", "").replace(" ", "_")
value = value.strip()
if not value and key.startswith(("Extended_renderer_info", "Memory_info")):
self._section = key[key.index("(") + 1:-1]
setattr(self, self._section, Container())
continue
if self._section:
if not key.startswith("____"):
self._section = None
else:
setattr(getattr(self, self._section), key.strip("_").lower(), value)
continue
self._attrs.add(key.lower())
setattr(self, key.lower(), value)
__init__(self, output=None)
special
¶
Creates a new GlxInfo object
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output |
str |
If provided, use this as the glxinfo output instead of running the program, useful for testing. |
None |
Source code in lutris/util/graphics/glxinfo.py
def __init__(self, output=None):
"""Creates a new GlxInfo object
Params:
output (str): If provided, use this as the glxinfo output instead
of running the program, useful for testing.
"""
self._output = output or self.get_glxinfo_output()
self._section = None
self._attrs = set() # Keep a reference of the created attributes
self.parse()
as_dict(self)
¶
Return the attributes as a dict
Source code in lutris/util/graphics/glxinfo.py
def as_dict(self):
"""Return the attributes as a dict"""
return {attr: getattr(self, attr) for attr in self._attrs}
get_glxinfo_output()
staticmethod
¶
Return the glxinfo -B output
Source code in lutris/util/graphics/glxinfo.py
@staticmethod
def get_glxinfo_output():
"""Return the glxinfo -B output"""
return read_process_output(["glxinfo", "-B"])
parse(self)
¶
Converts the glxinfo output to class attributes
Source code in lutris/util/graphics/glxinfo.py
def parse(self):
"""Converts the glxinfo output to class attributes"""
if not self._output:
logger.error("No available glxinfo output")
return
# Fix glxinfo output (Great, you saved one line by
# combining display and screen)
output = self._output.replace(" screen", "\nscreen")
for line in output.split("\n"):
if not line.strip():
continue
key, value = line.split(":", 1)
key = key.replace(" string", "").replace(" ", "_")
value = value.strip()
if not value and key.startswith(("Extended_renderer_info", "Memory_info")):
self._section = key[key.index("(") + 1:-1]
setattr(self, self._section, Container())
continue
if self._section:
if not key.startswith("____"):
self._section = None
else:
setattr(getattr(self, self._section), key.strip("_").lower(), value)
continue
self._attrs.add(key.lower())
setattr(self, key.lower(), value)
vkquery
¶
Query Vulkan capabilities
VK_ERROR_INITIALIZATION_FAILED
¶
VK_STRUCTURE_TYPE_APPLICATION_INFO
¶
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
¶
VK_SUCCESS
¶
VkInstance
¶
VkInstanceCreateFlags
¶
VkResult
¶
VkStructureType
¶
VkApplicationInfo (Structure)
¶
Python shim for struct VkApplicationInfo
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkApplicationInfo.html
Source code in lutris/util/graphics/vkquery.py
class VkApplicationInfo(Structure):
"""Python shim for struct VkApplicationInfo
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkApplicationInfo.html
"""
# pylint: disable=too-few-public-methods
_fields_ = [
("sType", VkStructureType),
("pNext", c_void_p),
("pApplicationName", c_char_p),
("applicationVersion", c_uint32),
("pEngineName", c_char_p),
("engineVersion", c_uint32),
("apiVersion", c_uint32),
]
def __init__(self, name, version):
super().__init__()
self.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
self.pApplicationName = name.encode()
self.applicationVersion = vk_make_version(*version)
self.apiVersion = vk_make_version(1, 0, 0)
__init__(self, name, version)
special
¶
Source code in lutris/util/graphics/vkquery.py
def __init__(self, name, version):
super().__init__()
self.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
self.pApplicationName = name.encode()
self.applicationVersion = vk_make_version(*version)
self.apiVersion = vk_make_version(1, 0, 0)
VkInstanceCreateInfo (Structure)
¶
Python shim for struct VkInstanceCreateInfo
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkInstanceCreateInfo.html
Source code in lutris/util/graphics/vkquery.py
class VkInstanceCreateInfo(Structure):
"""Python shim for struct VkInstanceCreateInfo
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkInstanceCreateInfo.html
"""
# pylint: disable=too-few-public-methods
_fields_ = [
("sType", VkStructureType),
("pNext", c_void_p),
("flags", VkInstanceCreateFlags),
("pApplicationInfo", POINTER(VkApplicationInfo)),
("enabledLayerCount", c_uint32),
("ppEnabledLayerNames", c_char_p),
("enabledExtensionCount", c_uint32),
("ppEnabledExtensionNames", c_char_p),
]
def __init__(self, app_info):
super().__init__()
self.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
self.pApplicationInfo = pointer(app_info)
__init__(self, app_info)
special
¶
Source code in lutris/util/graphics/vkquery.py
def __init__(self, app_info):
super().__init__()
self.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
self.pApplicationInfo = pointer(app_info)
is_vulkan_supported()
¶
Returns True iff vulkan library can be loaded, initialized, and reports at least one physical device available.
Source code in lutris/util/graphics/vkquery.py
def is_vulkan_supported():
"""
Returns True iff vulkan library can be loaded, initialized,
and reports at least one physical device available.
"""
vulkan = None
try:
vulkan = CDLL("libvulkan.so.1")
except OSError:
return False
app_info = VkApplicationInfo("vkinfo", version=(0, 1, 0))
create_info = VkInstanceCreateInfo(app_info)
instance = VkInstance()
result = vulkan.vkCreateInstance(byref(create_info), 0, byref(instance))
if result != VK_SUCCESS:
return False
dev_count = c_uint32(0)
result = vulkan.vkEnumeratePhysicalDevices(instance, byref(dev_count), 0)
vulkan.vkDestroyInstance(instance, 0)
return result == VK_SUCCESS and dev_count.value > 0
vk_make_version(major, minor, patch)
¶
VK_MAKE_VERSION macro logic for Python
Source code in lutris/util/graphics/vkquery.py
def vk_make_version(major, minor, patch):
"""
VK_MAKE_VERSION macro logic for Python
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#fundamentals-versionnum
"""
return c_uint32((major << 22) | (minor << 12) | patch)
xephyr
¶
Xephyr utilities
get_xephyr_command(display, config)
¶
Return a configured Xephyr command
Source code in lutris/util/graphics/xephyr.py
def get_xephyr_command(display, config):
"""Return a configured Xephyr command"""
xephyr_depth = "8" if config.get("xephyr") == "8bpp" else "16"
xephyr_resolution = config.get("xephyr_resolution") or "640x480"
xephyr_command = [
"Xephyr",
display,
"-ac",
"-screen",
xephyr_resolution + "x" + xephyr_depth,
"-glamor",
"-reset",
"-terminate",
]
if config.get("xephyr_fullscreen"):
xephyr_command.append("-fullscreen")
return xephyr_command
xrandr
¶
XrandR based display management
LegacyDisplayManager
¶
Legacy XrandR based display manager. Does not work on Wayland.
Source code in lutris/util/graphics/xrandr.py
class LegacyDisplayManager: # pylint: disable=too-few-public-methods
"""Legacy XrandR based display manager.
Does not work on Wayland.
"""
@staticmethod
def get_display_names():
"""Return output names from XrandR"""
return [output.name for output in get_outputs()]
@staticmethod
def get_resolutions():
"""Return available resolutions"""
return get_resolutions()
@staticmethod
def get_current_resolution():
"""Return the current resolution for the desktop"""
for line in _get_vidmodes():
if line.startswith(" ") and "*" in line:
resolution_match = re.match(r".*?(\d+x\d+).*", line)
if resolution_match:
return resolution_match.groups()[0].split("x")
return ("", "")
@staticmethod
def set_resolution(resolution):
"""Change the current resolution"""
change_resolution(resolution)
@staticmethod
def get_config():
"""Return the current display configuration"""
return get_outputs()
get_config()
staticmethod
¶
Return the current display configuration
Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_config():
"""Return the current display configuration"""
return get_outputs()
get_current_resolution()
staticmethod
¶
Return the current resolution for the desktop
Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_current_resolution():
"""Return the current resolution for the desktop"""
for line in _get_vidmodes():
if line.startswith(" ") and "*" in line:
resolution_match = re.match(r".*?(\d+x\d+).*", line)
if resolution_match:
return resolution_match.groups()[0].split("x")
return ("", "")
get_display_names()
staticmethod
¶
Return output names from XrandR
Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_display_names():
"""Return output names from XrandR"""
return [output.name for output in get_outputs()]
get_resolutions()
staticmethod
¶
Return available resolutions
Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_resolutions():
"""Return available resolutions"""
return get_resolutions()
set_resolution(resolution)
staticmethod
¶
Change the current resolution
Source code in lutris/util/graphics/xrandr.py
@staticmethod
def set_resolution(resolution):
"""Change the current resolution"""
change_resolution(resolution)
Output (tuple)
¶
Output(name, mode, position, rotation, primary, rate)
__getnewargs__(self)
special
¶
Return self as a plain tuple. Used by copy and pickle.
Source code in lutris/util/graphics/xrandr.py
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)
__new__(_cls, name, mode, position, rotation, primary, rate)
special
staticmethod
¶
Create new instance of Output(name, mode, position, rotation, primary, rate)
__repr__(self)
special
¶
Return a nicely formatted representation string
Source code in lutris/util/graphics/xrandr.py
def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + repr_fmt % self
change_resolution(resolution)
¶
Change display resolution.
Takes a string for single monitors or a list of displays as returned by get_outputs().
Source code in lutris/util/graphics/xrandr.py
def change_resolution(resolution):
"""Change display resolution.
Takes a string for single monitors or a list of displays as returned
by get_outputs().
"""
if not resolution:
logger.warning("No resolution provided")
return
if isinstance(resolution, str):
logger.debug("Switching resolution to %s", resolution)
if resolution not in get_resolutions():
logger.warning("Resolution %s doesn't exist.", resolution)
else:
logger.info("Changing resolution to %s", resolution)
with subprocess.Popen([LINUX_SYSTEM.get("xrandr"), "-s", resolution]) as xrandr:
xrandr.communicate()
else:
for display in resolution:
logger.debug("Switching to %s on %s", display.mode, display.name)
if display.rotation is not None and display.rotation in (
"normal",
"left",
"right",
"inverted",
):
rotation = display.rotation
else:
rotation = "normal"
logger.info("Switching resolution of %s to %s", display.name, display.mode)
with subprocess.Popen(
[
LINUX_SYSTEM.get("xrandr"),
"--output",
display.name,
"--mode",
display.mode,
"--pos",
display.position,
"--rotate",
rotation,
"--rate",
display.rate,
]
) as xrandr:
xrandr.communicate()
get_outputs()
¶
Return list of namedtuples containing output 'name', 'geometry', 'rotation' and whether it is the 'primary' display.
Source code in lutris/util/graphics/xrandr.py
def get_outputs(): # pylint: disable=too-many-locals
"""Return list of namedtuples containing output 'name', 'geometry',
'rotation' and whether it is the 'primary' display."""
outputs = []
vid_modes = _get_vidmodes()
position = None
rotate = None
primary = None
name = None
if not vid_modes:
logger.error("xrandr didn't return anything")
return []
for line in vid_modes:
if "connected" in line:
if "disconnected" in line:
continue
primary = "primary" in line
try:
if primary:
name, _, _, geometry, rotate, *_ = line.split()
else:
name, _, geometry, rotate, *_ = line.split()
except ValueError as ex:
logger.error(
"Unhandled xrandr line %s, error: %s. "
"Please send your xrandr output to the dev team", line, ex
)
continue
if geometry.startswith("("): # Screen turned off, no geometry
continue
if rotate.startswith("("): # Screen not rotated, no need to include
rotate = "normal"
_, x_pos, y_pos = geometry.split("+")
position = "{x_pos}x{y_pos}".format(x_pos=x_pos, y_pos=y_pos)
elif "*" in line:
mode, *framerates = line.split()
for number in framerates:
if "*" in number:
hertz = number[:-2]
outputs.append(
Output(
name=name,
mode=mode,
position=position,
rotation=rotate,
primary=primary,
rate=hertz,
)
)
break
return outputs
get_resolutions()
¶
Return the list of supported screen resolutions.
Source code in lutris/util/graphics/xrandr.py
def get_resolutions():
"""Return the list of supported screen resolutions."""
resolution_list = []
for line in _get_vidmodes():
if line.startswith(" "):
resolution_match = re.match(r".*?(\d+x\d+).*", line)
if resolution_match:
resolution_list.append(resolution_match.groups()[0])
return resolution_list
get_unique_resolutions()
¶
Return available resolutions, without duplicates and ordered with highest resolution first
Source code in lutris/util/graphics/xrandr.py
def get_unique_resolutions():
"""Return available resolutions, without duplicates and ordered with highest resolution first"""
return sorted(set(get_resolutions()), key=lambda x: int(x.split("x")[0]), reverse=True)
turn_off_except(display)
¶
Use XrandR to turn off displays except the one referenced by display
Source code in lutris/util/graphics/xrandr.py
def turn_off_except(display):
"""Use XrandR to turn off displays except the one referenced by `display`"""
if not display:
logger.error("No active display given, no turning off every display")
return
for output in get_outputs():
if output.name != display:
logger.info("Turning off %s", output[0])
with subprocess.Popen([LINUX_SYSTEM.get("xrandr"), "--output", output.name, "--off"]) as xrandr:
xrandr.communicate()
http
¶
HTTP utilities
DEFAULT_TIMEOUT
¶
HTTPError (Exception)
¶
Exception raised on request failures
Source code in lutris/util/http.py
class HTTPError(Exception):
"""Exception raised on request failures"""
def __init__(self, message, code=None):
super().__init__(message)
self.code = code
__init__(self, message, code=None)
special
¶
Source code in lutris/util/http.py
def __init__(self, message, code=None):
super().__init__(message)
self.code = code
Request
¶
Source code in lutris/util/http.py
class Request:
def __init__(
self,
url,
timeout=DEFAULT_TIMEOUT,
stop_request=None,
headers=None,
cookies=None,
):
self.url = self._clean_url(url)
self.status_code = None
self.content = b""
self.timeout = timeout
self.stop_request = stop_request
self.buffer_size = 1024 * 1024 # Bytes
self.total_size = None
self.downloaded_size = 0
self.headers = {"User-Agent": self.user_agent}
self.response_headers = None
self.info = None
if headers is None:
headers = {}
if not isinstance(headers, dict):
raise TypeError("HTTP headers needs to be a dict ({})".format(headers))
self.headers.update(headers)
if cookies:
cookie_processor = urllib.request.HTTPCookieProcessor(cookies)
self.opener = urllib.request.build_opener(cookie_processor)
else:
self.opener = None
@staticmethod
def _clean_url(url):
"""Checks that a given URL is valid and return a usable version"""
if not url:
raise ValueError("An URL is required!")
if url == "None":
raise ValueError("You'd better stop that right now.")
if url.startswith("//"):
url = "https:" + url
if url.startswith("/"):
logger.error("Stop using relative URLs!: %s", url)
url = SITE_URL + url
# That's for a single URL in EGS... not sure if we need more escaping
# The url received should already be receiving an escaped string
url = url.replace(" ", "%20")
return url
@property
def user_agent(self):
return "{} {}".format(PROJECT, VERSION)
def get(self, data=None):
logger.debug("GET %s", self.url)
try:
req = urllib.request.Request(url=self.url, data=data, headers=self.headers)
except ValueError as ex:
raise HTTPError("Failed to create HTTP request to %s: %s" % (self.url, ex)) from ex
try:
if self.opener:
request = self.opener.open(req, timeout=self.timeout)
else:
request = urllib.request.urlopen(req, timeout=self.timeout) # pylint: disable=consider-using-with
except (urllib.error.HTTPError, CertificateError) as error:
if error.code == 401:
raise UnauthorizedAccess("Access to %s denied" % self.url) from error
raise HTTPError("%s" % error, code=error.code) from error
except (socket.timeout, urllib.error.URLError) as error:
raise HTTPError("Unable to connect to server %s: %s" % (self.url, error)) from error
self.response_headers = request.getheaders()
self.status_code = request.getcode()
if self.status_code > 299:
logger.warning("Request responded with code %s", self.status_code)
try:
self.total_size = int(request.info().get("Content-Length").strip())
except AttributeError:
self.total_size = 0
self.content = b"".join(self._iter_chunks(request))
self.info = request.info()
request.close()
return self
def _iter_chunks(self, request):
while 1:
if self.stop_request and self.stop_request.is_set():
self.content = b""
return self
try:
chunk = request.read(self.buffer_size)
except (socket.timeout, ConnectionResetError) as err:
raise HTTPError("Request timed out") from err
self.downloaded_size += len(chunk)
if not chunk:
return
yield chunk
def post(self, data):
raise NotImplementedError
def write_to_file(self, path):
content = self.content
logger.debug("Writing to %s", path)
if not content:
logger.warning("No content to write")
return
dirname = os.path.dirname(path)
if not system.path_exists(dirname):
os.makedirs(dirname)
with open(path, "wb") as dest_file:
dest_file.write(content)
@property
def json(self):
_raw_json = self.text
if _raw_json:
try:
return json.loads(_raw_json)
except json.decoder.JSONDecodeError as err:
raise ValueError(f"JSON response from {self.url} could not be decoded: '{_raw_json[:80]}'") from err
return {}
@property
def text(self):
if self.content:
return self.content.decode()
return ""
json
property
readonly
¶
text
property
readonly
¶
user_agent
property
readonly
¶
__init__(self, url, timeout=30, stop_request=None, headers=None, cookies=None)
special
¶
Source code in lutris/util/http.py
def __init__(
self,
url,
timeout=DEFAULT_TIMEOUT,
stop_request=None,
headers=None,
cookies=None,
):
self.url = self._clean_url(url)
self.status_code = None
self.content = b""
self.timeout = timeout
self.stop_request = stop_request
self.buffer_size = 1024 * 1024 # Bytes
self.total_size = None
self.downloaded_size = 0
self.headers = {"User-Agent": self.user_agent}
self.response_headers = None
self.info = None
if headers is None:
headers = {}
if not isinstance(headers, dict):
raise TypeError("HTTP headers needs to be a dict ({})".format(headers))
self.headers.update(headers)
if cookies:
cookie_processor = urllib.request.HTTPCookieProcessor(cookies)
self.opener = urllib.request.build_opener(cookie_processor)
else:
self.opener = None
get(self, data=None)
¶
Source code in lutris/util/http.py
def get(self, data=None):
logger.debug("GET %s", self.url)
try:
req = urllib.request.Request(url=self.url, data=data, headers=self.headers)
except ValueError as ex:
raise HTTPError("Failed to create HTTP request to %s: %s" % (self.url, ex)) from ex
try:
if self.opener:
request = self.opener.open(req, timeout=self.timeout)
else:
request = urllib.request.urlopen(req, timeout=self.timeout) # pylint: disable=consider-using-with
except (urllib.error.HTTPError, CertificateError) as error:
if error.code == 401:
raise UnauthorizedAccess("Access to %s denied" % self.url) from error
raise HTTPError("%s" % error, code=error.code) from error
except (socket.timeout, urllib.error.URLError) as error:
raise HTTPError("Unable to connect to server %s: %s" % (self.url, error)) from error
self.response_headers = request.getheaders()
self.status_code = request.getcode()
if self.status_code > 299:
logger.warning("Request responded with code %s", self.status_code)
try:
self.total_size = int(request.info().get("Content-Length").strip())
except AttributeError:
self.total_size = 0
self.content = b"".join(self._iter_chunks(request))
self.info = request.info()
request.close()
return self
post(self, data)
¶
Source code in lutris/util/http.py
def post(self, data):
raise NotImplementedError
write_to_file(self, path)
¶
Source code in lutris/util/http.py
def write_to_file(self, path):
content = self.content
logger.debug("Writing to %s", path)
if not content:
logger.warning("No content to write")
return
dirname = os.path.dirname(path)
if not system.path_exists(dirname):
os.makedirs(dirname)
with open(path, "wb") as dest_file:
dest_file.write(content)
UnauthorizedAccess (Exception)
¶
Exception raised for 401 HTTP errors
Source code in lutris/util/http.py
class UnauthorizedAccess(Exception):
"""Exception raised for 401 HTTP errors"""
download_file(url, dest, overwrite=False, raise_errors=False)
¶
Save a remote resource locally
Source code in lutris/util/http.py
def download_file(url, dest, overwrite=False, raise_errors=False):
"""Save a remote resource locally"""
if system.path_exists(dest):
if overwrite:
os.remove(dest)
else:
return dest
if not url:
return None
try:
request = Request(url).get()
except HTTPError as ex:
if raise_errors:
raise
logger.error("Failed to get url %s: %s", url, ex)
return None
request.write_to_file(dest)
return dest
i18n
¶
Language and translation utilities
get_lang()
¶
Return the 2 letter language code used by the system
Source code in lutris/util/i18n.py
def get_lang():
"""Return the 2 letter language code used by the system"""
user_locale = get_user_locale()
if not user_locale:
return ""
return user_locale[:2]
get_lang_and_country()
¶
Return language code and country for the current user
Source code in lutris/util/i18n.py
def get_lang_and_country():
"""Return language code and country for the current user"""
user_locale = get_user_locale()
if not user_locale:
return "", ""
lang_code, country = user_locale.split('-' if '-' in locale else '_')
return lang_code, country
get_user_locale()
¶
Source code in lutris/util/i18n.py
def get_user_locale():
user_locale, _user_encoding = locale.getlocale()
if not user_locale:
logger.error("Unable to get locale")
return
return user_locale
jobs
¶
AsyncCall (Thread)
¶
Source code in lutris/util/jobs.py
class AsyncCall(threading.Thread):
def __init__(self, func, callback, *args, **kwargs):
"""Execute `function` in a new thread then schedule `callback` for
execution in the main loop.
"""
self.source_id = None
self.stop_request = threading.Event()
super().__init__(target=self.target, args=args, kwargs=kwargs)
self.function = func
self.callback = callback if callback else lambda r, e: None
self.daemon = kwargs.pop("daemon", True)
self.start()
def target(self, *args, **kwargs):
result = None
error = None
try:
result = self.function(*args, **kwargs)
except Exception as ex: # pylint: disable=broad-except
logger.error("Error while completing task %s: %s %s", self.function, type(ex), ex)
error = ex
_ex_type, _ex_value, trace = sys.exc_info()
traceback.print_tb(trace)
self.source_id = GLib.idle_add(self.callback, result, error)
return self.source_id
__init__(self, func, callback, *args, **kwargs)
special
¶
Execute function in a new thread then schedule callback for
execution in the main loop.
Source code in lutris/util/jobs.py
def __init__(self, func, callback, *args, **kwargs):
"""Execute `function` in a new thread then schedule `callback` for
execution in the main loop.
"""
self.source_id = None
self.stop_request = threading.Event()
super().__init__(target=self.target, args=args, kwargs=kwargs)
self.function = func
self.callback = callback if callback else lambda r, e: None
self.daemon = kwargs.pop("daemon", True)
self.start()
target(self, *args, **kwargs)
¶
Source code in lutris/util/jobs.py
def target(self, *args, **kwargs):
result = None
error = None
try:
result = self.function(*args, **kwargs)
except Exception as ex: # pylint: disable=broad-except
logger.error("Error while completing task %s: %s %s", self.function, type(ex), ex)
error = ex
_ex_type, _ex_value, trace = sys.exc_info()
traceback.print_tb(trace)
self.source_id = GLib.idle_add(self.callback, result, error)
return self.source_id
synchronized_call(func, event, result)
¶
Calls func, stores the result by reference, set an event when finished
Source code in lutris/util/jobs.py
def synchronized_call(func, event, result):
"""Calls func, stores the result by reference, set an event when finished"""
result.append(func())
event.set()
thread_safe_call(func)
¶
Synchronous call to func, safe to call in a callback started from a thread Not safe to use otherwise, will crash if run from the main thread.
See: https://pygobject.readthedocs.io/en/latest/guide/threading.html
Source code in lutris/util/jobs.py
def thread_safe_call(func):
"""Synchronous call to func, safe to call in a callback started from a thread
Not safe to use otherwise, will crash if run from the main thread.
See: https://pygobject.readthedocs.io/en/latest/guide/threading.html
"""
event = threading.Event()
result = []
GLib.idle_add(synchronized_call, func, event, result)
event.wait()
return result[0]
joypad
¶
get_controller_mappings()
¶
Source code in lutris/util/joypad.py
def get_controller_mappings():
devices = get_devices()
controller_db = GameControllerDB()
controllers = []
for device in devices:
guid = get_sdl_identifier(device.info)
if guid in controller_db.controllers:
controllers.append((device, controller_db[guid]))
return controllers
get_devices()
¶
Source code in lutris/util/joypad.py
def get_devices():
if not evdev:
logger.warning("python3-evdev not installed, controller support not available")
return []
_devices = []
for dev in evdev.list_devices():
try:
_devices.append(evdev.InputDevice(dev))
except RuntimeError:
pass
return _devices
get_joypads()
¶
Return a list of tuples with the device and the joypad name
Source code in lutris/util/joypad.py
def get_joypads():
"""Return a list of tuples with the device and the joypad name"""
return [(dev.fn, dev.name) for dev in get_devices()]
get_sdl_identifier(device_info)
¶
Source code in lutris/util/joypad.py
def get_sdl_identifier(device_info):
device_identifier = struct.pack(
"<LLLL",
device_info.bustype,
device_info.vendor,
device_info.product,
device_info.version,
)
return binascii.hexlify(device_identifier).decode()
read_button(device)
¶
Reference function for reading controller buttons and axis values. Not to be used as is.
Source code in lutris/util/joypad.py
def read_button(device):
"""Reference function for reading controller buttons and axis values.
Not to be used as is.
"""
# pylint: disable=no-member
for event in device.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 0:
print("button %s (%s): %s" % (event.code, hex(event.code), event.value))
if event.type == evdev.ecodes.EV_ABS:
sticks = (0, 1, 3, 4)
if event.code not in sticks or abs(event.value) > 5000:
print("axis %s (%s): %s" % (event.code, hex(event.code), event.value))
keyring
¶
libretro
¶
RetroConfig
¶
Source code in lutris/util/libretro.py
class RetroConfig:
value_map = {"true": True, "false": False, "": None}
def __init__(self, config_path):
if not config_path:
raise ValueError("Config path is mandatory")
self.config_path = config_path
self._config = []
@property
def config(self):
"""Lazy loading of the RetroArch config """
if self._config:
return self._config
try:
self.load_config()
return self._config
except UnicodeDecodeError:
logger.error(
"The Retroarch config in %s could not "
"be read because of character encoding issues",
self.config_path
)
return []
def load_config(self):
"""Load the configuration from file"""
self._config = []
if not os.path.isfile(self.config_path):
raise OSError("Specified config file {} does not exist".format(self.config_path))
with open(self.config_path, "r", encoding='utf-8') as config_file:
for line in config_file.readlines():
if not line:
continue
line = line.strip()
if line == "" or line.startswith('#'):
continue
if '=' in line:
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"')
if not key or not value:
continue
self._config.append((key, value))
def save(self):
with open(self.config_path, "w", encoding='utf-8') as config_file:
for (key, value) in self.config:
config_file.write('{} = "{}"\n'.format(key, value))
def serialize_value(self, value):
for k, v in self.value_map.items():
if value is v:
return k
return value
def deserialize_value(self, value):
for k, v in self.value_map.items():
if value == k:
return v
return value
def __getitem__(self, key):
for k, value in self.config:
if key == k:
return self.deserialize_value(value)
def __setitem__(self, key, value):
for index, conf in enumerate(self.config):
if key == conf[0]:
# self.config is read-only
self._config[index] = (key, self.serialize_value(value))
return
self._config.append((key, self.serialize_value(value)))
def keys(self):
return [key for (key, _value) in self.config]
config
property
readonly
¶
Lazy loading of the RetroArch config
value_map
¶
__getitem__(self, key)
special
¶
Source code in lutris/util/libretro.py
def __getitem__(self, key):
for k, value in self.config:
if key == k:
return self.deserialize_value(value)
__init__(self, config_path)
special
¶
Source code in lutris/util/libretro.py
def __init__(self, config_path):
if not config_path:
raise ValueError("Config path is mandatory")
self.config_path = config_path
self._config = []
__setitem__(self, key, value)
special
¶
Source code in lutris/util/libretro.py
def __setitem__(self, key, value):
for index, conf in enumerate(self.config):
if key == conf[0]:
# self.config is read-only
self._config[index] = (key, self.serialize_value(value))
return
self._config.append((key, self.serialize_value(value)))
deserialize_value(self, value)
¶
Source code in lutris/util/libretro.py
def deserialize_value(self, value):
for k, v in self.value_map.items():
if value == k:
return v
return value
keys(self)
¶
Source code in lutris/util/libretro.py
def keys(self):
return [key for (key, _value) in self.config]
load_config(self)
¶
Load the configuration from file
Source code in lutris/util/libretro.py
def load_config(self):
"""Load the configuration from file"""
self._config = []
if not os.path.isfile(self.config_path):
raise OSError("Specified config file {} does not exist".format(self.config_path))
with open(self.config_path, "r", encoding='utf-8') as config_file:
for line in config_file.readlines():
if not line:
continue
line = line.strip()
if line == "" or line.startswith('#'):
continue
if '=' in line:
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"')
if not key or not value:
continue
self._config.append((key, value))
save(self)
¶
Source code in lutris/util/libretro.py
def save(self):
with open(self.config_path, "w", encoding='utf-8') as config_file:
for (key, value) in self.config:
config_file.write('{} = "{}"\n'.format(key, value))
serialize_value(self, value)
¶
Source code in lutris/util/libretro.py
def serialize_value(self, value):
for k, v in self.value_map.items():
if value is v:
return k
return value
linux
¶
Linux specific platform code
LINUX_SYSTEM
¶
SYSTEM_COMPONENTS
¶
linux_distribution
¶
LinuxSystem
¶
Global cache for system commands
Source code in lutris/util/linux.py
class LinuxSystem: # pylint: disable=too-many-public-methods
"""Global cache for system commands"""
_cache = {}
multiarch_lib_folders = [
("/lib", "/lib64"),
("/lib32", "/lib64"),
("/usr/lib", "/usr/lib64"),
("/usr/lib32", "/usr/lib64"),
("/lib/i386-linux-gnu", "/lib/x86_64-linux-gnu"),
("/usr/lib/i386-linux-gnu", "/usr/lib/x86_64-linux-gnu"),
]
soundfont_folders = [
"/usr/share/sounds/sf2",
"/usr/share/soundfonts",
]
recommended_no_file_open = 524288
required_components = ["OPENGL", "VULKAN", "GNUTLS"]
optional_components = ["WINE", "GAMEMODE"]
flatpak_info_path = "/.flatpak-info"
def __init__(self):
for key in ("COMMANDS", "TERMINALS"):
self._cache[key] = {}
for command in SYSTEM_COMPONENTS[key]:
command_path = shutil.which(command)
if not command_path:
command_path = self.get_sbin_path(command)
if command_path:
self._cache[key][command] = command_path
# Detect if system is 64bit capable
self.is_64_bit = sys.maxsize > 2**32
self.arch = self.get_arch()
self.shared_libraries = self.get_shared_libraries()
self.populate_libraries()
self.populate_sound_fonts()
self.soft_limit, self.hard_limit = self.get_file_limits()
self.glxinfo = self.get_glxinfo()
@staticmethod
def get_sbin_path(command):
"""Some distributions don't put sbin directories in $PATH"""
path_candidates = ["/sbin", "/usr/sbin"]
for candidate in path_candidates:
command_path = os.path.join(candidate, command)
if os.path.exists(command_path):
return command_path
@staticmethod
def get_file_limits():
return resource.getrlimit(resource.RLIMIT_NOFILE)
def has_enough_file_descriptors(self):
return self.hard_limit >= self.recommended_no_file_open
@staticmethod
def get_cpus():
"""Parse the output of /proc/cpuinfo"""
cpus = [{}]
cpu_index = 0
with open("/proc/cpuinfo", encoding='utf-8') as cpuinfo:
for line in cpuinfo.readlines():
if not line.strip():
cpu_index += 1
cpus.append({})
continue
key, value = line.split(":", 1)
cpus[cpu_index][key.strip()] = value.strip()
return [cpu for cpu in cpus if cpu]
@staticmethod
def get_drives():
"""Return a list of drives with their filesystems"""
lsblk_output = system.read_process_output(["lsblk", "-f", "--json"])
return [
drive
for drive in json.loads(lsblk_output)["blockdevices"]
if drive["fstype"] != "squashfs"
]
@staticmethod
def get_ram_info():
"""Parse the output of /proc/meminfo and return RAM information in kB"""
mem = {}
with open("/proc/meminfo", encoding='utf-8') as meminfo:
for line in meminfo.readlines():
key, value = line.split(":", 1)
mem[key.strip()] = value.strip('kB \n')
return mem
@staticmethod
def get_dist_info():
"""Return distribution information"""
if linux_distribution:
return linux_distribution()
return "unknown"
@staticmethod
def get_arch():
"""Return the system architecture only if compatible
with the supported architectures from the Lutris API
"""
machine = platform.machine()
if machine == "x86_64":
return "x86_64"
if machine in ("i386", "i686"):
return "i386"
if "armv7" in machine:
return "armv7"
logger.warning("Unsupported architecture %s", machine)
@staticmethod
def get_kernel_version():
"""Get kernel info from /proc/version"""
with open("/proc/version", encoding='utf-8') as kernel_info:
info = kernel_info.readlines()[0]
version = info.split(" ")[2]
return version
def gamemode_available(self):
"""Return whether gamemode is available"""
# Current versions of gamemode use gamemoderun
if system.find_executable("gamemoderun"):
return True
# This is for old versions of gamemode only
if self.is_feature_supported("GAMEMODE"):
return True
return False
@property
def has_steam(self):
"""Return whether Steam is installed locally"""
return bool(system.find_executable("steam"))
@property
def display_server(self):
"""Return the display server used"""
return os.environ.get("XDG_SESSION_TYPE", "unknown")
@property
def is_flatpak(self):
"""Check is we are running inside Flatpak sandbox"""
return system.path_exists(self.flatpak_info_path)
@property
def runtime_architectures(self):
"""Return the architectures supported on this machine"""
if self.arch == "x86_64":
return ["i386", "x86_64"]
return ["i386"]
@property
def requirements(self):
return self.get_requirements()
@property
def critical_requirements(self):
return self.get_requirements(include_optional=False)
def get_fs_type_for_path(self, path):
"""Return the filesystem type a given path uses"""
path_drive = system.get_drive_for_path(path)
for drive in self.get_drives():
for partition in drive.get("children", []):
if "/dev/%s" % partition["name"] == path_drive:
return partition["fstype"]
def get_glxinfo(self):
"""Return a GlxInfo instance if the gfxinfo tool is available"""
if not self.get("glxinfo"):
return
_glxinfo = glxinfo.GlxInfo()
if not hasattr(_glxinfo, "display"):
logger.warning("Invalid glxinfo received")
return
return _glxinfo
def get_requirements(self, include_optional=True):
"""Return used system requirements"""
_requirements = self.required_components.copy()
if include_optional:
_requirements += self.optional_components
if drivers.is_amd():
_requirements.append("RADEON")
return _requirements
def get(self, command):
"""Return a system command path if available"""
return self._cache["COMMANDS"].get(command)
def get_terminals(self):
"""Return list of installed terminals"""
return list(self._cache["TERMINALS"].values())
def get_soundfonts(self):
"""Return path of available soundfonts"""
return self._cache["SOUNDFONTS"]
def get_lib_folders(self):
"""Return shared library folders, sorted by most used to least used"""
lib_folder_counter = Counter(lib.dirname for lib_list in self.shared_libraries.values() for lib in lib_list)
return [path[0] for path in lib_folder_counter.most_common()]
def iter_lib_folders(self):
"""Loop over existing 32/64 bit library folders"""
exported_lib_folders = set()
for lib_folder in self.get_lib_folders():
exported_lib_folders.add(lib_folder)
yield lib_folder
for lib_paths in self.multiarch_lib_folders:
if self.arch != "x86_64":
# On non amd64 setups, only the first element is relevant
lib_paths = [lib_paths[0]]
else:
# Ignore paths where 64-bit path is link to supposed 32-bit path
if os.path.realpath(lib_paths[0]) == os.path.realpath(lib_paths[1]):
continue
if all(os.path.exists(path) for path in lib_paths):
if lib_paths[0] not in exported_lib_folders:
yield lib_paths[0]
if len(lib_paths) != 1:
if lib_paths[1] not in exported_lib_folders:
yield lib_paths[1]
def get_ldconfig_libs(self):
"""Return a list of available libraries, as returned by `ldconfig -p`."""
ldconfig = self.get("ldconfig")
if not ldconfig:
logger.error("Could not detect ldconfig on this system")
return []
output = system.read_process_output([ldconfig, "-p"]).split("\n")
return [line.strip("\t") for line in output if line.startswith("\t")]
def get_shared_libraries(self):
"""Loads all available libraries on the system as SharedLibrary instances
The libraries are stored in a defaultdict keyed by library name.
"""
shared_libraries = defaultdict(list)
for lib_line in self.get_ldconfig_libs():
try:
lib = SharedLibrary.new_from_ldconfig(lib_line)
except ValueError:
logger.error("Invalid ldconfig line: %s", lib_line)
continue
if lib.arch not in self.runtime_architectures:
continue
shared_libraries[lib.name].append(lib)
return shared_libraries
def populate_libraries(self):
"""Populates the LIBRARIES cache with what is found on the system"""
self._cache["LIBRARIES"] = {}
for arch in self.runtime_architectures:
self._cache["LIBRARIES"][arch] = defaultdict(list)
for req in self.requirements:
for lib in SYSTEM_COMPONENTS["LIBRARIES"][req]:
for shared_lib in self.shared_libraries[lib]:
self._cache["LIBRARIES"][shared_lib.arch][req].append(lib)
def populate_sound_fonts(self):
"""Populates the soundfont cache"""
self._cache["SOUNDFONTS"] = []
for folder in self.soundfont_folders:
if not os.path.exists(folder):
continue
for soundfont in os.listdir(folder):
self._cache["SOUNDFONTS"].append(soundfont)
def get_missing_requirement_libs(self, req):
"""Return a list of sets of missing libraries for each supported architecture"""
required_libs = set(SYSTEM_COMPONENTS["LIBRARIES"][req])
return [list(required_libs - set(self._cache["LIBRARIES"][arch][req])) for arch in self.runtime_architectures]
def get_missing_libs(self):
"""Return a dictionary of missing libraries"""
return {req: self.get_missing_requirement_libs(req) for req in self.requirements}
def is_feature_supported(self, feature):
"""Return whether the system has the necessary libs to support a feature"""
if feature == "ACO":
try:
mesa_version = LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version
return mesa_version >= "19.3"
except AttributeError:
return False
return not self.get_missing_requirement_libs(feature)[0]
critical_requirements
property
readonly
¶
display_server
property
readonly
¶
Return the display server used
flatpak_info_path
¶
has_steam
property
readonly
¶
Return whether Steam is installed locally
is_flatpak
property
readonly
¶
Check is we are running inside Flatpak sandbox
multiarch_lib_folders
¶
optional_components
¶
recommended_no_file_open
¶
required_components
¶
requirements
property
readonly
¶
runtime_architectures
property
readonly
¶
Return the architectures supported on this machine
soundfont_folders
¶
__init__(self)
special
¶
Source code in lutris/util/linux.py
def __init__(self):
for key in ("COMMANDS", "TERMINALS"):
self._cache[key] = {}
for command in SYSTEM_COMPONENTS[key]:
command_path = shutil.which(command)
if not command_path:
command_path = self.get_sbin_path(command)
if command_path:
self._cache[key][command] = command_path
# Detect if system is 64bit capable
self.is_64_bit = sys.maxsize > 2**32
self.arch = self.get_arch()
self.shared_libraries = self.get_shared_libraries()
self.populate_libraries()
self.populate_sound_fonts()
self.soft_limit, self.hard_limit = self.get_file_limits()
self.glxinfo = self.get_glxinfo()
gamemode_available(self)
¶
Return whether gamemode is available
Source code in lutris/util/linux.py
def gamemode_available(self):
"""Return whether gamemode is available"""
# Current versions of gamemode use gamemoderun
if system.find_executable("gamemoderun"):
return True
# This is for old versions of gamemode only
if self.is_feature_supported("GAMEMODE"):
return True
return False
get(self, command)
¶
Return a system command path if available
Source code in lutris/util/linux.py
def get(self, command):
"""Return a system command path if available"""
return self._cache["COMMANDS"].get(command)
get_arch()
staticmethod
¶
Return the system architecture only if compatible with the supported architectures from the Lutris API
Source code in lutris/util/linux.py
@staticmethod
def get_arch():
"""Return the system architecture only if compatible
with the supported architectures from the Lutris API
"""
machine = platform.machine()
if machine == "x86_64":
return "x86_64"
if machine in ("i386", "i686"):
return "i386"
if "armv7" in machine:
return "armv7"
logger.warning("Unsupported architecture %s", machine)
get_cpus()
staticmethod
¶
Parse the output of /proc/cpuinfo
Source code in lutris/util/linux.py
@staticmethod
def get_cpus():
"""Parse the output of /proc/cpuinfo"""
cpus = [{}]
cpu_index = 0
with open("/proc/cpuinfo", encoding='utf-8') as cpuinfo:
for line in cpuinfo.readlines():
if not line.strip():
cpu_index += 1
cpus.append({})
continue
key, value = line.split(":", 1)
cpus[cpu_index][key.strip()] = value.strip()
return [cpu for cpu in cpus if cpu]
get_dist_info()
staticmethod
¶
Return distribution information
Source code in lutris/util/linux.py
@staticmethod
def get_dist_info():
"""Return distribution information"""
if linux_distribution:
return linux_distribution()
return "unknown"
get_drives()
staticmethod
¶
Return a list of drives with their filesystems
Source code in lutris/util/linux.py
@staticmethod
def get_drives():
"""Return a list of drives with their filesystems"""
lsblk_output = system.read_process_output(["lsblk", "-f", "--json"])
return [
drive
for drive in json.loads(lsblk_output)["blockdevices"]
if drive["fstype"] != "squashfs"
]
get_file_limits()
staticmethod
¶
Source code in lutris/util/linux.py
@staticmethod
def get_file_limits():
return resource.getrlimit(resource.RLIMIT_NOFILE)
get_fs_type_for_path(self, path)
¶
Return the filesystem type a given path uses
Source code in lutris/util/linux.py
def get_fs_type_for_path(self, path):
"""Return the filesystem type a given path uses"""
path_drive = system.get_drive_for_path(path)
for drive in self.get_drives():
for partition in drive.get("children", []):
if "/dev/%s" % partition["name"] == path_drive:
return partition["fstype"]
get_glxinfo(self)
¶
Return a GlxInfo instance if the gfxinfo tool is available
Source code in lutris/util/linux.py
def get_glxinfo(self):
"""Return a GlxInfo instance if the gfxinfo tool is available"""
if not self.get("glxinfo"):
return
_glxinfo = glxinfo.GlxInfo()
if not hasattr(_glxinfo, "display"):
logger.warning("Invalid glxinfo received")
return
return _glxinfo
get_kernel_version()
staticmethod
¶
Get kernel info from /proc/version
Source code in lutris/util/linux.py
@staticmethod
def get_kernel_version():
"""Get kernel info from /proc/version"""
with open("/proc/version", encoding='utf-8') as kernel_info:
info = kernel_info.readlines()[0]
version = info.split(" ")[2]
return version
get_ldconfig_libs(self)
¶
Return a list of available libraries, as returned by ldconfig -p.
Source code in lutris/util/linux.py
def get_ldconfig_libs(self):
"""Return a list of available libraries, as returned by `ldconfig -p`."""
ldconfig = self.get("ldconfig")
if not ldconfig:
logger.error("Could not detect ldconfig on this system")
return []
output = system.read_process_output([ldconfig, "-p"]).split("\n")
return [line.strip("\t") for line in output if line.startswith("\t")]
get_lib_folders(self)
¶
Return shared library folders, sorted by most used to least used
Source code in lutris/util/linux.py
def get_lib_folders(self):
"""Return shared library folders, sorted by most used to least used"""
lib_folder_counter = Counter(lib.dirname for lib_list in self.shared_libraries.values() for lib in lib_list)
return [path[0] for path in lib_folder_counter.most_common()]
get_missing_libs(self)
¶
Return a dictionary of missing libraries
Source code in lutris/util/linux.py
def get_missing_libs(self):
"""Return a dictionary of missing libraries"""
return {req: self.get_missing_requirement_libs(req) for req in self.requirements}
get_missing_requirement_libs(self, req)
¶
Return a list of sets of missing libraries for each supported architecture
Source code in lutris/util/linux.py
def get_missing_requirement_libs(self, req):
"""Return a list of sets of missing libraries for each supported architecture"""
required_libs = set(SYSTEM_COMPONENTS["LIBRARIES"][req])
return [list(required_libs - set(self._cache["LIBRARIES"][arch][req])) for arch in self.runtime_architectures]
get_ram_info()
staticmethod
¶
Parse the output of /proc/meminfo and return RAM information in kB
Source code in lutris/util/linux.py
@staticmethod
def get_ram_info():
"""Parse the output of /proc/meminfo and return RAM information in kB"""
mem = {}
with open("/proc/meminfo", encoding='utf-8') as meminfo:
for line in meminfo.readlines():
key, value = line.split(":", 1)
mem[key.strip()] = value.strip('kB \n')
return mem
get_requirements(self, include_optional=True)
¶
Return used system requirements
Source code in lutris/util/linux.py
def get_requirements(self, include_optional=True):
"""Return used system requirements"""
_requirements = self.required_components.copy()
if include_optional:
_requirements += self.optional_components
if drivers.is_amd():
_requirements.append("RADEON")
return _requirements
get_sbin_path(command)
staticmethod
¶
Some distributions don't put sbin directories in $PATH
Source code in lutris/util/linux.py
@staticmethod
def get_sbin_path(command):
"""Some distributions don't put sbin directories in $PATH"""
path_candidates = ["/sbin", "/usr/sbin"]
for candidate in path_candidates:
command_path = os.path.join(candidate, command)
if os.path.exists(command_path):
return command_path
get_shared_libraries(self)
¶
Loads all available libraries on the system as SharedLibrary instances The libraries are stored in a defaultdict keyed by library name.
Source code in lutris/util/linux.py
def get_shared_libraries(self):
"""Loads all available libraries on the system as SharedLibrary instances
The libraries are stored in a defaultdict keyed by library name.
"""
shared_libraries = defaultdict(list)
for lib_line in self.get_ldconfig_libs():
try:
lib = SharedLibrary.new_from_ldconfig(lib_line)
except ValueError:
logger.error("Invalid ldconfig line: %s", lib_line)
continue
if lib.arch not in self.runtime_architectures:
continue
shared_libraries[lib.name].append(lib)
return shared_libraries
get_soundfonts(self)
¶
Return path of available soundfonts
Source code in lutris/util/linux.py
def get_soundfonts(self):
"""Return path of available soundfonts"""
return self._cache["SOUNDFONTS"]
get_terminals(self)
¶
Return list of installed terminals
Source code in lutris/util/linux.py
def get_terminals(self):
"""Return list of installed terminals"""
return list(self._cache["TERMINALS"].values())
has_enough_file_descriptors(self)
¶
Source code in lutris/util/linux.py
def has_enough_file_descriptors(self):
return self.hard_limit >= self.recommended_no_file_open
is_feature_supported(self, feature)
¶
Return whether the system has the necessary libs to support a feature
Source code in lutris/util/linux.py
def is_feature_supported(self, feature):
"""Return whether the system has the necessary libs to support a feature"""
if feature == "ACO":
try:
mesa_version = LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version
return mesa_version >= "19.3"
except AttributeError:
return False
return not self.get_missing_requirement_libs(feature)[0]
iter_lib_folders(self)
¶
Loop over existing 32/64 bit library folders
Source code in lutris/util/linux.py
def iter_lib_folders(self):
"""Loop over existing 32/64 bit library folders"""
exported_lib_folders = set()
for lib_folder in self.get_lib_folders():
exported_lib_folders.add(lib_folder)
yield lib_folder
for lib_paths in self.multiarch_lib_folders:
if self.arch != "x86_64":
# On non amd64 setups, only the first element is relevant
lib_paths = [lib_paths[0]]
else:
# Ignore paths where 64-bit path is link to supposed 32-bit path
if os.path.realpath(lib_paths[0]) == os.path.realpath(lib_paths[1]):
continue
if all(os.path.exists(path) for path in lib_paths):
if lib_paths[0] not in exported_lib_folders:
yield lib_paths[0]
if len(lib_paths) != 1:
if lib_paths[1] not in exported_lib_folders:
yield lib_paths[1]
populate_libraries(self)
¶
Populates the LIBRARIES cache with what is found on the system
Source code in lutris/util/linux.py
def populate_libraries(self):
"""Populates the LIBRARIES cache with what is found on the system"""
self._cache["LIBRARIES"] = {}
for arch in self.runtime_architectures:
self._cache["LIBRARIES"][arch] = defaultdict(list)
for req in self.requirements:
for lib in SYSTEM_COMPONENTS["LIBRARIES"][req]:
for shared_lib in self.shared_libraries[lib]:
self._cache["LIBRARIES"][shared_lib.arch][req].append(lib)
populate_sound_fonts(self)
¶
Populates the soundfont cache
Source code in lutris/util/linux.py
def populate_sound_fonts(self):
"""Populates the soundfont cache"""
self._cache["SOUNDFONTS"] = []
for folder in self.soundfont_folders:
if not os.path.exists(folder):
continue
for soundfont in os.listdir(folder):
self._cache["SOUNDFONTS"].append(soundfont)
SharedLibrary
¶
Representation of a Linux shared library
Source code in lutris/util/linux.py
class SharedLibrary:
"""Representation of a Linux shared library"""
default_arch = "i386"
def __init__(self, name, flags, path):
self.name = name
self.flags = [flag.strip() for flag in flags.split(",")]
self.path = path
@classmethod
def new_from_ldconfig(cls, ldconfig_line):
"""Create a SharedLibrary instance from an output line from ldconfig"""
lib_match = re.match(r"^(.*) \((.*)\) => (.*)$", ldconfig_line)
if not lib_match:
raise ValueError("Received incorrect value for ldconfig line: %s" % ldconfig_line)
return cls(lib_match.group(1), lib_match.group(2), lib_match.group(3))
@property
def arch(self):
"""Return the architecture for a shared library"""
detected_arch = ["x86-64", "x32"]
for arch in detected_arch:
if arch in self.flags:
return arch.replace("-", "_")
return self.default_arch
@property
def basename(self):
"""Return the name of the library without an extention"""
return self.name.split(".so")[0]
@property
def dirname(self):
"""Return the directory where the lib resides"""
return os.path.dirname(self.path)
def __str__(self):
return "%s (%s)" % (self.name, self.arch)
arch
property
readonly
¶
Return the architecture for a shared library
basename
property
readonly
¶
Return the name of the library without an extention
default_arch
¶
dirname
property
readonly
¶
Return the directory where the lib resides
__init__(self, name, flags, path)
special
¶
Source code in lutris/util/linux.py
def __init__(self, name, flags, path):
self.name = name
self.flags = [flag.strip() for flag in flags.split(",")]
self.path = path
__str__(self)
special
¶
Source code in lutris/util/linux.py
def __str__(self):
return "%s (%s)" % (self.name, self.arch)
new_from_ldconfig(ldconfig_line)
classmethod
¶
Create a SharedLibrary instance from an output line from ldconfig
Source code in lutris/util/linux.py
@classmethod
def new_from_ldconfig(cls, ldconfig_line):
"""Create a SharedLibrary instance from an output line from ldconfig"""
lib_match = re.match(r"^(.*) \((.*)\) => (.*)$", ldconfig_line)
if not lib_match:
raise ValueError("Received incorrect value for ldconfig line: %s" % ldconfig_line)
return cls(lib_match.group(1), lib_match.group(2), lib_match.group(3))
gather_system_info()
¶
Get all system information in a single data structure
Source code in lutris/util/linux.py
def gather_system_info():
"""Get all system information in a single data structure"""
system_info = {}
if drivers.is_nvidia():
system_info["nvidia_driver"] = drivers.get_nvidia_driver_info()
system_info["nvidia_gpus"] = [drivers.get_nvidia_gpu_info(gpu_id) for gpu_id in drivers.get_nvidia_gpu_ids()]
system_info["gpus"] = [drivers.get_gpu_info(gpu) for gpu in drivers.get_gpus()]
system_info["env"] = dict(os.environ)
system_info["missing_libs"] = LINUX_SYSTEM.get_missing_libs()
system_info["cpus"] = LINUX_SYSTEM.get_cpus()
system_info["drives"] = LINUX_SYSTEM.get_drives()
system_info["ram"] = LINUX_SYSTEM.get_ram_info()
system_info["dist"] = LINUX_SYSTEM.get_dist_info()
system_info["arch"] = LINUX_SYSTEM.get_arch()
system_info["kernel"] = LINUX_SYSTEM.get_kernel_version()
system_info["glxinfo"] = glxinfo.GlxInfo().as_dict()
return system_info
gather_system_info_str()
¶
Get all relevant system information already formatted as a string
Source code in lutris/util/linux.py
def gather_system_info_str():
"""Get all relevant system information already formatted as a string"""
system_info = gather_system_info()
system_info_readable = {}
# Add system information
system_dict = {}
system_dict["OS"] = ' '.join(system_info["dist"])
system_dict["Arch"] = system_info["arch"]
system_dict["Kernel"] = system_info["kernel"]
system_dict["Desktop"] = system_info["env"].get("XDG_CURRENT_DESKTOP", "Not found")
system_dict["Display Server"] = system_info["env"].get("XDG_SESSION_TYPE", "Not found")
system_info_readable["System"] = system_dict
# Add CPU information
cpu_dict = {}
cpu_dict["Vendor"] = system_info["cpus"][0].get("vendor_id", "Vendor unavailable")
cpu_dict["Model"] = system_info["cpus"][0].get("model name", "Model unavailable")
cpu_dict["Physical cores"] = system_info["cpus"][0].get("cpu cores", "Physical cores unavailable")
cpu_dict["Logical cores"] = system_info["cpus"][0].get("siblings", "Logical cores unavailable")
system_info_readable["CPU"] = cpu_dict
# Add memory information
ram_dict = {}
ram_dict["RAM"] = "%0.1f GB" % (float(system_info["ram"]["MemTotal"]) / 1024 / 1024)
ram_dict["Swap"] = "%0.1f GB" % (float(system_info["ram"]["SwapTotal"]) / 1024 / 1024)
system_info_readable["Memory"] = ram_dict
# Add graphics information
graphics_dict = {}
if LINUX_SYSTEM.glxinfo:
graphics_dict["Vendor"] = system_info["glxinfo"].get("opengl_vendor", "Vendor unavailable")
graphics_dict["OpenGL Renderer"] = system_info["glxinfo"].get("opengl_renderer", "OpenGL Renderer unavailable")
graphics_dict["OpenGL Version"] = system_info["glxinfo"].get("opengl_version", "OpenGL Version unavailable")
graphics_dict["OpenGL Core"] = system_info["glxinfo"].get(
"opengl_core_profile_version", "OpenGL core unavailable"
)
graphics_dict["OpenGL ES"] = system_info["glxinfo"].get("opengl_es_profile_version", "OpenGL ES unavailable")
else:
graphics_dict["Vendor"] = "Unable to obtain glxinfo"
# check Vulkan support
if vkquery.is_vulkan_supported():
graphics_dict["Vulkan"] = "Supported"
else:
graphics_dict["Vulkan"] = "Not Supported"
system_info_readable["Graphics"] = graphics_dict
output = ''
for section, dictionary in system_info_readable.items():
output += '[%s]\n' % section
for key, value in dictionary.items():
tabs = " " * (16 - len(key))
output += '%s:%s%s\n' % (key, tabs, value)
output += '\n'
return output
get_default_terminal()
¶
Return the default terminal emulator
Source code in lutris/util/linux.py
def get_default_terminal():
"""Return the default terminal emulator"""
terms = get_terminal_apps()
if terms:
return terms[0]
logger.error("Couldn't find a terminal emulator.")
get_terminal_apps()
¶
Return the list of installed terminal emulators
Source code in lutris/util/linux.py
def get_terminal_apps():
"""Return the list of installed terminal emulators"""
return LINUX_SYSTEM.get_terminals()
log
¶
magic
¶
magic is a wrapper around the libmagic file identification library.
See https://github.com/ahupp/python-magic for more information.
Usage:
import magic magic.from_file("testdata/test.pdf") 'PDF document, version 1.2' magic.from_file("testdata/test.pdf", mime=True) 'application/pdf' magic.from_buffer(open("testdata/test.pdf").read(1024)) 'PDF document, version 1.2'
MAGIC_CHECK
¶
MAGIC_COMPRESS
¶
MAGIC_CONTINUE
¶
MAGIC_DEBUG
¶
MAGIC_DEVICES
¶
MAGIC_ERROR
¶
MAGIC_EXTENSION
¶
MAGIC_MIME
¶
MAGIC_MIME_ENCODING
¶
MAGIC_MIME_TYPE
¶
MAGIC_NONE
¶
MAGIC_NO_CHECK_APPTYPE
¶
MAGIC_NO_CHECK_ASCII
¶
MAGIC_NO_CHECK_COMPRESS
¶
MAGIC_NO_CHECK_ELF
¶
MAGIC_NO_CHECK_FORTRAN
¶
MAGIC_NO_CHECK_SOFT
¶
MAGIC_NO_CHECK_TAR
¶
MAGIC_NO_CHECK_TOKENS
¶
MAGIC_NO_CHECK_TROFF
¶
MAGIC_PARAM_BYTES_MAX
¶
MAGIC_PARAM_ELF_NOTES_MAX
¶
MAGIC_PARAM_ELF_PHNUM_MAX
¶
MAGIC_PARAM_ELF_SHNUM_MAX
¶
MAGIC_PARAM_INDIR_MAX
¶
MAGIC_PARAM_NAME_MAX
¶
MAGIC_PARAM_REGEX_MAX
¶
MAGIC_PRESERVE_ATIME
¶
MAGIC_RAW
¶
MAGIC_SYMLINK
¶
dll
¶
libmagic
¶
magic_check
¶
magic_close
¶
magic_compile
¶
magic_errno
¶
magic_error
¶
magic_open
¶
magic_setflags
¶
magic_t
¶
magic_version
¶
Magic
¶
Magic is a wrapper around the libmagic C library.
Source code in lutris/util/magic.py
class Magic:
"""
Magic is a wrapper around the libmagic C library.
"""
def __init__(self, mime=False, magic_file=None, mime_encoding=False, # pylint: disable=redefined-outer-name
keep_going=False, uncompress=False, raw=False, extension=False):
"""
Create a new libmagic wrapper.
mime - if True, mimetypes are returned instead of textual descriptions
mime_encoding - if True, codec is returned
magic_file - use a mime database other than the system default
keep_going - don't stop at the first match, keep going
uncompress - Try to look inside compressed files.
raw - Do not try to decode "non-printable" chars.
extension - Print a slash-separated list of valid extensions for the file type found.
"""
self.cookie = None
self.flags = MAGIC_NONE
if mime:
self.flags |= MAGIC_MIME_TYPE
if mime_encoding:
self.flags |= MAGIC_MIME_ENCODING
if keep_going:
self.flags |= MAGIC_CONTINUE
if uncompress:
self.flags |= MAGIC_COMPRESS
if raw:
self.flags |= MAGIC_RAW
if extension:
self.flags |= MAGIC_EXTENSION
self.cookie = magic_open(self.flags)
self.lock = threading.Lock()
magic_load(self.cookie, magic_file)
# MAGIC_EXTENSION was added in 523 or 524, so bail if
# it doesn't appear to be available
if extension and (not _has_version or version() < 524):
raise NotImplementedError('MAGIC_EXTENSION is not supported in this version of libmagic')
# For https://github.com/ahupp/python-magic/issues/190
# libmagic has fixed internal limits that some files exceed, causing
# an error. We can avoid this (at least for the sample file given)
# by bumping the limit up. It's not clear if this is a general solution
# or whether other internal limits should be increased, but given
# the lack of other reports I'll assume this is rare.
if _has_param:
try:
self.setparam(MAGIC_PARAM_NAME_MAX, 64)
except MagicException:
# some versions of libmagic fail this call,
# so rather than fail hard just use default behavior
pass
def from_buffer(self, buf):
"""
Identify the contents of `buf`
"""
with self.lock:
try:
# if we're on python3, convert buf to bytes
# otherwise this string is passed as wchar*
# which is not what libmagic expects
if isinstance(buf, str) and str != bytes:
buf = buf.encode('utf-8', errors='replace')
return maybe_decode(magic_buffer(self.cookie, buf))
except MagicException as e:
return self._handle509Bug(e)
def from_file(self, filename):
# raise FileNotFoundException or IOError if the file does not exist
with _real_open(filename):
pass
with self.lock:
try:
return maybe_decode(magic_file(self.cookie, filename))
except MagicException as e:
return self._handle509Bug(e)
def from_descriptor(self, fd):
with self.lock:
try:
return maybe_decode(magic_descriptor(self.cookie, fd))
except MagicException as e:
return self._handle509Bug(e)
def _handle509Bug(self, e):
# libmagic 5.09 has a bug where it might fail to identify the
# mimetype of a file and returns null from magic_file (and
# likely _buffer), but also does not return an error message.
if e.message is None and (self.flags & MAGIC_MIME_TYPE):
return "application/octet-stream"
raise e
def setparam(self, param, val):
return magic_setparam(self.cookie, param, val)
def getparam(self, param):
return magic_getparam(self.cookie, param)
def __del__(self):
# no _thread_check here because there can be no other
# references to this object at this point.
# during shutdown magic_close may have been cleared already so
# make sure it exists before using it.
# the self.cookie check should be unnecessary and was an
# incorrect fix for a threading problem, however I'm leaving
# it in because it's harmless and I'm slightly afraid to
# remove it.
if self.cookie and magic_close:
magic_close(self.cookie)
self.cookie = None
__del__(self)
special
¶
Source code in lutris/util/magic.py
def __del__(self):
# no _thread_check here because there can be no other
# references to this object at this point.
# during shutdown magic_close may have been cleared already so
# make sure it exists before using it.
# the self.cookie check should be unnecessary and was an
# incorrect fix for a threading problem, however I'm leaving
# it in because it's harmless and I'm slightly afraid to
# remove it.
if self.cookie and magic_close:
magic_close(self.cookie)
self.cookie = None
__init__(self, mime=False, magic_file=None, mime_encoding=False, keep_going=False, uncompress=False, raw=False, extension=False)
special
¶
Create a new libmagic wrapper.
mime - if True, mimetypes are returned instead of textual descriptions mime_encoding - if True, codec is returned magic_file - use a mime database other than the system default keep_going - don't stop at the first match, keep going uncompress - Try to look inside compressed files. raw - Do not try to decode "non-printable" chars. extension - Print a slash-separated list of valid extensions for the file type found.
Source code in lutris/util/magic.py
def __init__(self, mime=False, magic_file=None, mime_encoding=False, # pylint: disable=redefined-outer-name
keep_going=False, uncompress=False, raw=False, extension=False):
"""
Create a new libmagic wrapper.
mime - if True, mimetypes are returned instead of textual descriptions
mime_encoding - if True, codec is returned
magic_file - use a mime database other than the system default
keep_going - don't stop at the first match, keep going
uncompress - Try to look inside compressed files.
raw - Do not try to decode "non-printable" chars.
extension - Print a slash-separated list of valid extensions for the file type found.
"""
self.cookie = None
self.flags = MAGIC_NONE
if mime:
self.flags |= MAGIC_MIME_TYPE
if mime_encoding:
self.flags |= MAGIC_MIME_ENCODING
if keep_going:
self.flags |= MAGIC_CONTINUE
if uncompress:
self.flags |= MAGIC_COMPRESS
if raw:
self.flags |= MAGIC_RAW
if extension:
self.flags |= MAGIC_EXTENSION
self.cookie = magic_open(self.flags)
self.lock = threading.Lock()
magic_load(self.cookie, magic_file)
# MAGIC_EXTENSION was added in 523 or 524, so bail if
# it doesn't appear to be available
if extension and (not _has_version or version() < 524):
raise NotImplementedError('MAGIC_EXTENSION is not supported in this version of libmagic')
# For https://github.com/ahupp/python-magic/issues/190
# libmagic has fixed internal limits that some files exceed, causing
# an error. We can avoid this (at least for the sample file given)
# by bumping the limit up. It's not clear if this is a general solution
# or whether other internal limits should be increased, but given
# the lack of other reports I'll assume this is rare.
if _has_param:
try:
self.setparam(MAGIC_PARAM_NAME_MAX, 64)
except MagicException:
# some versions of libmagic fail this call,
# so rather than fail hard just use default behavior
pass
from_buffer(self, buf)
¶
Identify the contents of buf
Source code in lutris/util/magic.py
def from_buffer(self, buf):
"""
Identify the contents of `buf`
"""
with self.lock:
try:
# if we're on python3, convert buf to bytes
# otherwise this string is passed as wchar*
# which is not what libmagic expects
if isinstance(buf, str) and str != bytes:
buf = buf.encode('utf-8', errors='replace')
return maybe_decode(magic_buffer(self.cookie, buf))
except MagicException as e:
return self._handle509Bug(e)
from_descriptor(self, fd)
¶
Source code in lutris/util/magic.py
def from_descriptor(self, fd):
with self.lock:
try:
return maybe_decode(magic_descriptor(self.cookie, fd))
except MagicException as e:
return self._handle509Bug(e)
from_file(self, filename)
¶
Source code in lutris/util/magic.py
def from_file(self, filename):
# raise FileNotFoundException or IOError if the file does not exist
with _real_open(filename):
pass
with self.lock:
try:
return maybe_decode(magic_file(self.cookie, filename))
except MagicException as e:
return self._handle509Bug(e)
getparam(self, param)
¶
Source code in lutris/util/magic.py
def getparam(self, param):
return magic_getparam(self.cookie, param)
setparam(self, param, val)
¶
Source code in lutris/util/magic.py
def setparam(self, param, val):
return magic_setparam(self.cookie, param, val)
MagicException (Exception)
¶
Source code in lutris/util/magic.py
class MagicException(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message
__init__(self, message)
special
¶
Source code in lutris/util/magic.py
def __init__(self, message):
super().__init__(message)
self.message = message
coerce_filename(filename)
¶
Source code in lutris/util/magic.py
def coerce_filename(filename):
if filename is None:
return None
# ctypes will implicitly convert unicode strings to bytes with
# .encode('ascii'). If you use the filesystem encoding
# then you'll get inconsistent behavior (crashes) depending on the user's
# LANG environment variable
if isinstance(filename, str):
return filename.encode('utf-8', 'surrogateescape')
return filename
errorcheck_negative_one(result, func, args)
¶
Source code in lutris/util/magic.py
def errorcheck_negative_one(result, func, args):
if result == -1:
err = magic_error(args[0])
raise MagicException(err)
return result
errorcheck_null(result, func, args)
¶
Source code in lutris/util/magic.py
def errorcheck_null(result, func, args):
if result is None:
err = magic_error(args[0])
raise MagicException(err)
return result
from_buffer(buffer, mime=False)
¶
Accepts a binary string and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.
magic.from_buffer(open("testdata/test.pdf").read(1024)) 'PDF document, version 1.2'
Source code in lutris/util/magic.py
def from_buffer(buffer, mime=False):
"""
Accepts a binary string and returns the detected filetype. Return
value is the mimetype if mime=True, otherwise a human readable
name.
>>> magic.from_buffer(open("testdata/test.pdf").read(1024))
'PDF document, version 1.2'
"""
m = _get_magic_type(mime)
return m.from_buffer(buffer)
from_descriptor(fd, mime=False)
¶
Accepts a file descriptor and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.
f = open("testdata/test.pdf") magic.from_descriptor(f.fileno()) 'PDF document, version 1.2'
Source code in lutris/util/magic.py
def from_descriptor(fd, mime=False):
"""
Accepts a file descriptor and returns the detected filetype. Return
value is the mimetype if mime=True, otherwise a human readable
name.
>>> f = open("testdata/test.pdf")
>>> magic.from_descriptor(f.fileno())
'PDF document, version 1.2'
"""
m = _get_magic_type(mime)
return m.from_descriptor(fd)
from_file(filename, mime=False)
¶
" Accepts a filename and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.
magic.from_file("testdata/test.pdf", mime=True) 'application/pdf'
Source code in lutris/util/magic.py
def from_file(filename, mime=False):
""""
Accepts a filename and returns the detected filetype. Return
value is the mimetype if mime=True, otherwise a human readable
name.
>>> magic.from_file("testdata/test.pdf", mime=True)
'application/pdf'
"""
m = _get_magic_type(mime)
return m.from_file(filename)
magic_buffer(cookie, buf)
¶
Source code in lutris/util/magic.py
def magic_buffer(cookie, buf):
return _magic_buffer(cookie, buf, len(buf))
magic_descriptor(cookie, fd)
¶
Source code in lutris/util/magic.py
def magic_descriptor(cookie, fd):
return _magic_descriptor(cookie, fd)
magic_file(cookie, filename)
¶
Source code in lutris/util/magic.py
def magic_file(cookie, filename):
return _magic_file(cookie, coerce_filename(filename))
magic_getparam(cookie, param)
¶
Source code in lutris/util/magic.py
def magic_getparam(cookie, param):
if not _has_param:
raise NotImplementedError("magic_getparam not implemented")
val = c_size_t()
_magic_getparam(cookie, param, byref(val))
return val.value
magic_load(cookie, filename)
¶
Source code in lutris/util/magic.py
def magic_load(cookie, filename):
return _magic_load(cookie, coerce_filename(filename))
magic_setparam(cookie, param, val)
¶
Source code in lutris/util/magic.py
def magic_setparam(cookie, param, val):
if not _has_param:
raise NotImplementedError("magic_setparam not implemented")
v = c_size_t(val)
return _magic_setparam(cookie, param, byref(v))
maybe_decode(s)
¶
Source code in lutris/util/magic.py
def maybe_decode(s):
if str == bytes:
return s
# backslashreplace here because sometimes libmagic will return metadata in the charset
# of the file, which is unknown to us (e.g the title of a Word doc)
return s.decode('utf-8', 'backslashreplace')
version()
¶
Source code in lutris/util/magic.py
def version():
if not _has_version:
raise NotImplementedError("magic_version not implemented")
return magic_version()
mame
special
¶
database
¶
Utility functions for MAME
CACHE_DIR
¶
get_games(xml_path)
¶
Return a list of all games
Source code in lutris/util/mame/database.py
def get_games(xml_path):
"""Return a list of all games"""
return {
machine.attrib["name"]: get_machine_info(machine)
for machine in iter_machines(xml_path, is_game)
}
get_machine_info(machine)
¶
Return human readable information about a machine node
Source code in lutris/util/mame/database.py
def get_machine_info(machine):
"""Return human readable information about a machine node"""
return {
"description": machine.find("description").text,
"manufacturer": simplify_manufacturer(machine.find("manufacturer").text),
"year": machine.find("year").text,
"roms": [rom.attrib for rom in machine.findall("rom")],
"ports": [port.attrib for port in machine.findall("port")],
"devices": [
{
"info": device.attrib,
"name": "".join(
[instance.attrib["name"] for instance in device.findall("instance")]
),
"briefname": "".join(
[
instance.attrib["briefname"]
for instance in device.findall("instance")
]
),
"extensions": [
extension.attrib["name"]
for extension in device.findall("extension")
],
}
for device in machine.findall("device")
],
"input": machine.find("input").attrib,
"driver": machine.find("driver").attrib,
}
get_supported_systems(xml_path, force=False)
¶
Return supported systems (computers and consoles) supported. From the full XML list extracted from MAME, filter the systems that are runnable, not clones and have the ability to run software.
Source code in lutris/util/mame/database.py
def get_supported_systems(xml_path, force=False):
"""Return supported systems (computers and consoles) supported.
From the full XML list extracted from MAME, filter the systems that are
runnable, not clones and have the ability to run software.
"""
systems_cache_path = os.path.join(CACHE_DIR, "systems.json")
if os.path.exists(systems_cache_path) and not force:
with open(systems_cache_path, "r", encoding='utf-8') as systems_cache_file:
try:
systems = json.load(systems_cache_file)
except json.JSONDecodeError:
logger.error("Failed to read systems cache %s", systems_cache_path)
systems = None
if systems:
return systems
systems = {
machine.attrib["name"]: get_machine_info(machine)
for machine in iter_machines(xml_path, is_system)
}
if not systems:
return {}
with open(systems_cache_path, "w", encoding='utf-8') as systems_cache_file:
json.dump(systems, systems_cache_file, indent=2)
return systems
has_software_list(machine)
¶
Return True if the machine has an associated software list
Source code in lutris/util/mame/database.py
def has_software_list(machine):
"""Return True if the machine has an associated software list"""
_has_software_list = False
for elem in machine:
if elem.tag == "device_ref" and elem.attrib["name"] == "software_list":
_has_software_list = True
return _has_software_list
is_game(machine)
¶
Return True if the given machine game is an original arcade game Clones return False
Source code in lutris/util/mame/database.py
def is_game(machine):
"""Return True if the given machine game is an original arcade game
Clones return False
"""
return (
machine.attrib["isbios"] == "no"
and machine.attrib["isdevice"] == "no"
and machine.attrib["runnable"] == "yes"
and "romof" not in machine.attrib
# FIXME: Filter by the machines that accept coins, but not like that
# and "coin" in machine.find("input").attrib
)
is_system(machine)
¶
Given a machine XML tag, return True if it is a computer, console or handheld.
Source code in lutris/util/mame/database.py
def is_system(machine):
"""Given a machine XML tag, return True if it is a computer, console or
handheld.
"""
if (
machine.attrib.get("runnable") == "no"
or machine.attrib.get("isdevice") == "yes"
or machine.attrib.get("isbios") == "yes"
):
return False
return has_software_list(machine)
iter_machines(xml_path, filter_func=None)
¶
Iterate through machine nodes in the MAME XML
Source code in lutris/util/mame/database.py
def iter_machines(xml_path, filter_func=None):
"""Iterate through machine nodes in the MAME XML"""
try:
root = ElementTree.parse(xml_path).getroot()
except Exception as ex: # pylint: disable=broad-except
logger.error("Failed to read MAME XML: %s", ex)
return []
for machine in root:
if filter_func and not filter_func(machine):
continue
yield machine
simplify_manufacturer(manufacturer)
¶
Give simplified names for some manufacturers
Source code in lutris/util/mame/database.py
def simplify_manufacturer(manufacturer):
"""Give simplified names for some manufacturers"""
manufacturer_map = {
"Amstrad plc": "Amstrad",
"Apple Computer": "Apple",
"Commodore Business Machines": "Commodore",
}
return manufacturer_map.get(manufacturer, manufacturer)
ini
¶
Manipulate MAME ini files
MameIni
¶
Looks like an ini file and yet it is not one!
Source code in lutris/util/mame/ini.py
class MameIni:
"""Looks like an ini file and yet it is not one!"""
def __init__(self, ini_path):
if not path_exists(ini_path):
raise OSError("File %s does not exist" % ini_path)
self.ini_path = ini_path
self.lines = []
self.config = {}
def parse(self, line):
"""Store configuration value from a line"""
line = line.strip()
if not line or line.startswith("#"):
return None, None
key, *_value = line.split(maxsplit=1)
if _value:
return key, _value[0]
return key, None
def read(self):
"""Reads the content of the ini file"""
with open(self.ini_path, "r", encoding='utf-8') as ini_file:
for line in ini_file.readlines():
self.lines.append(line)
print(line)
config_key, config_value = self.parse(line)
if config_key:
self.config[config_key] = config_value
def write(self):
"""Writes the file to disk"""
with open(self.ini_path, "w", encoding='utf-8') as ini_file:
for line in self.lines:
config_key, _value = self.parse(line)
if config_key and self.config[config_key]:
ini_file.write("%-26s%s\n" % (config_key, self.config[config_key]))
elif config_key:
ini_file.write("%s\n" % config_key)
else:
ini_file.write(line)
__init__(self, ini_path)
special
¶
Source code in lutris/util/mame/ini.py
def __init__(self, ini_path):
if not path_exists(ini_path):
raise OSError("File %s does not exist" % ini_path)
self.ini_path = ini_path
self.lines = []
self.config = {}
parse(self, line)
¶
Store configuration value from a line
Source code in lutris/util/mame/ini.py
def parse(self, line):
"""Store configuration value from a line"""
line = line.strip()
if not line or line.startswith("#"):
return None, None
key, *_value = line.split(maxsplit=1)
if _value:
return key, _value[0]
return key, None
read(self)
¶
Reads the content of the ini file
Source code in lutris/util/mame/ini.py
def read(self):
"""Reads the content of the ini file"""
with open(self.ini_path, "r", encoding='utf-8') as ini_file:
for line in ini_file.readlines():
self.lines.append(line)
print(line)
config_key, config_value = self.parse(line)
if config_key:
self.config[config_key] = config_value
write(self)
¶
Writes the file to disk
Source code in lutris/util/mame/ini.py
def write(self):
"""Writes the file to disk"""
with open(self.ini_path, "w", encoding='utf-8') as ini_file:
for line in self.lines:
config_key, _value = self.parse(line)
if config_key and self.config[config_key]:
ini_file.write("%-26s%s\n" % (config_key, self.config[config_key]))
elif config_key:
ini_file.write("%s\n" % config_key)
else:
ini_file.write(line)
nvidia
¶
Nvidia library detection from Proton
RTLD_DI_LINKMAP
¶
LinkMap (Structure)
¶
from dlinfo(3)
struct link_map { ElfW(Addr) l_addr; / Difference between the address in the ELF file and the address in memory / char l_name; / Absolute pathname where object was found / ElfW(Dyn) l_ld; / Dynamic section of the shared object / struct link_map l_next, l_prev; / Chain of loaded objects / / Plus additional fields private to the implementation / };
Source code in lutris/util/nvidia.py
class LinkMap(Structure):
"""
from dlinfo(3)
struct link_map {
ElfW(Addr) l_addr; /* Difference between the
address in the ELF file and
the address in memory */
char *l_name; /* Absolute pathname where
object was found */
ElfW(Dyn) *l_ld; /* Dynamic section of the
shared object */
struct link_map *l_next, *l_prev;
/* Chain of loaded objects */
/* Plus additional fields private to the implementation */
};
"""
_fields_ = [("l_addr", c_void_p), ("l_name", c_char_p), ("l_ld", c_void_p)]
get_nvidia_dll_path()
¶
Return the path to the location of DLL files for use by Wine/Proton from the NVIDIA Linux driver. See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for background on the chosen method of DLL discovery.
Source code in lutris/util/nvidia.py
def get_nvidia_dll_path():
"""Return the path to the location of DLL files for use by Wine/Proton
from the NVIDIA Linux driver.
See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for
background on the chosen method of DLL discovery.
"""
libglx_path = get_nvidia_glx_path()
if not libglx_path:
logger.warning("Unable to locate libGLX_nvidia")
return
nvidia_wine_dir = os.path.join(os.path.dirname(libglx_path), "nvidia/wine")
if os.path.exists(os.path.join(nvidia_wine_dir, "nvngx.dll")):
return nvidia_wine_dir
get_nvidia_glx_path()
¶
Return the absolute path to the libGLX_nvidia library
Source code in lutris/util/nvidia.py
def get_nvidia_glx_path():
"""Return the absolute path to the libGLX_nvidia library"""
try:
libdl = CDLL("libdl.so.2")
except OSError:
logger.error("Unable to load libdl.so.2")
return None
try:
libglx_nvidia = CDLL("libGLX_nvidia.so.0")
except OSError:
logger.error("Unable to load libGLX_nvidia.so.0")
return None
# from dlinfo(3)
#
# int dlinfo (void *restrict handle, int request, void *restrict info)
dlinfo_func = libdl.dlinfo
dlinfo_func.argtypes = c_void_p, c_int, c_void_p
dlinfo_func.restype = c_int
# Allocate a LinkMap object
glx_nvidia_info_ptr = POINTER(LinkMap)()
# Run dlinfo(3) on the handle to libGLX_nvidia.so.0, storing results at the
# address represented by glx_nvidia_info_ptr
if (
dlinfo_func(
libglx_nvidia._handle, RTLD_DI_LINKMAP, addressof(glx_nvidia_info_ptr)
) != 0
):
logger.error("Unable to read Nvidia information")
return None
# Grab the contents our of our pointer
glx_nvidia_info = cast(glx_nvidia_info_ptr, POINTER(LinkMap)).contents
# Decode the path to our library to a str()
if glx_nvidia_info.l_name is None:
logger.error("Error reading the Nvidia library path")
return None
try:
libglx_nvidia_path = os.fsdecode(glx_nvidia_info.l_name)
except UnicodeDecodeError as ex:
logger.error("Error decoding the Nvidia library path: %s", ex)
return None
# Follow any symlinks to the actual file
return os.path.realpath(libglx_nvidia_path)
process
¶
Class to manipulate a process
IGNORED_PROCESSES
¶
InvalidPid (Exception)
¶
Exception raised when an operation on a non-existent PID is called
Source code in lutris/util/process.py
class InvalidPid(Exception):
"""Exception raised when an operation on a non-existent PID is called"""
Process
¶
Python abstraction a Linux process
Source code in lutris/util/process.py
class Process:
"""Python abstraction a Linux process"""
def __init__(self, pid):
try:
self.pid = int(pid)
self.error_cache = []
except ValueError as err:
raise InvalidPid("'%s' is not a valid pid" % pid) from err
def __repr__(self):
return "Process {}".format(self.pid)
def __str__(self):
return "{} ({}:{})".format(self.name, self.pid, self.state)
def _read_content(self, file_path):
"""Return the contents from a file in /proc"""
try:
with open(file_path, encoding='utf-8') as proc_file:
content = proc_file.read()
except (ProcessLookupError, FileNotFoundError, PermissionError):
return ""
return content
def get_stat(self, parsed=True):
stat_filename = "/proc/{}/stat".format(self.pid)
try:
with open(stat_filename, encoding='utf-8') as stat_file:
_stat = stat_file.readline()
except (ProcessLookupError, FileNotFoundError):
return None
if parsed:
return _stat[_stat.rfind(")") + 1:].split()
return _stat
def get_thread_ids(self):
"""Return a list of thread ids opened by process."""
basedir = "/proc/{}/task/".format(self.pid)
if os.path.isdir(basedir):
try:
return os.listdir(basedir)
except FileNotFoundError:
return []
else:
return []
def get_children_pids_of_thread(self, tid):
"""Return pids of child processes opened by thread `tid` of process."""
children_path = "/proc/{}/task/{}/children".format(self.pid, tid)
try:
with open(children_path, encoding='utf-8') as children_file:
children_content = children_file.read()
except (FileNotFoundError, ProcessLookupError):
children_content = ""
return children_content.strip().split()
@property
def name(self):
"""Filename of the executable."""
_stat = self.get_stat(parsed=False)
if _stat:
return _stat[_stat.find("(") + 1:_stat.rfind(")")]
return None
@property
def state(self):
"""One character from the string "RSDZTW" where R is running, S is
sleeping in an interruptible wait, D is waiting in uninterruptible disk
sleep, Z is zombie, T is traced or stopped (on a signal), and W is
paging.
"""
_stat = self.get_stat()
if _stat:
return _stat[0]
return None
@property
def cmdline(self):
"""Return command line used to run the process `pid`."""
cmdline_path = "/proc/{}/cmdline".format(self.pid)
_cmdline_content = self._read_content(cmdline_path)
if _cmdline_content:
return _cmdline_content.replace("\x00", " ").replace("\\", "/")
@property
def cwd(self):
"""Return current working dir of process"""
cwd_path = "/proc/%d/cwd" % int(self.pid)
return os.readlink(cwd_path)
@property
def environ(self):
"""Return the process' environment variables"""
environ_path = "/proc/{}/environ".format(self.pid)
_environ_text = self._read_content(environ_path)
if not _environ_text:
return {}
try:
return dict([line.split("=", 1) for line in _environ_text.split("\x00") if line])
except ValueError:
if environ_path not in self.error_cache:
logger.error("Failed to parse environment variables: %s", _environ_text)
self.error_cache.append(environ_path)
return {}
@property
def children(self):
"""Return the child processes of this process"""
_children = []
for tid in self.get_thread_ids():
for child_pid in self.get_children_pids_of_thread(tid):
_children.append(Process(child_pid))
return _children
def iter_children(self):
"""Iterator that yields all the children of a process"""
for child in self.children:
yield child
yield from child.iter_children()
def wait_for_finish(self):
"""Waits until the process finishes
This only works if self.pid is a child process of Lutris
"""
try:
pid, ret_status = os.waitpid(int(self.pid) * -1, 0)
except OSError as ex:
logger.error("Failed to get exit status for PID %s", self.pid)
logger.error(ex)
return -1
logger.info("PID %s exited with code %s", pid, ret_status)
return ret_status
children
property
readonly
¶
Return the child processes of this process
cmdline
property
readonly
¶
Return command line used to run the process pid.
cwd
property
readonly
¶
Return current working dir of process
environ
property
readonly
¶
Return the process' environment variables
name
property
readonly
¶
Filename of the executable.
state
property
readonly
¶
One character from the string "RSDZTW" where R is running, S is sleeping in an interruptible wait, D is waiting in uninterruptible disk sleep, Z is zombie, T is traced or stopped (on a signal), and W is paging.
__init__(self, pid)
special
¶
Source code in lutris/util/process.py
def __init__(self, pid):
try:
self.pid = int(pid)
self.error_cache = []
except ValueError as err:
raise InvalidPid("'%s' is not a valid pid" % pid) from err
__repr__(self)
special
¶
Source code in lutris/util/process.py
def __repr__(self):
return "Process {}".format(self.pid)
__str__(self)
special
¶
Source code in lutris/util/process.py
def __str__(self):
return "{} ({}:{})".format(self.name, self.pid, self.state)
get_children_pids_of_thread(self, tid)
¶
Return pids of child processes opened by thread tid of process.
Source code in lutris/util/process.py
def get_children_pids_of_thread(self, tid):
"""Return pids of child processes opened by thread `tid` of process."""
children_path = "/proc/{}/task/{}/children".format(self.pid, tid)
try:
with open(children_path, encoding='utf-8') as children_file:
children_content = children_file.read()
except (FileNotFoundError, ProcessLookupError):
children_content = ""
return children_content.strip().split()
get_stat(self, parsed=True)
¶
Source code in lutris/util/process.py
def get_stat(self, parsed=True):
stat_filename = "/proc/{}/stat".format(self.pid)
try:
with open(stat_filename, encoding='utf-8') as stat_file:
_stat = stat_file.readline()
except (ProcessLookupError, FileNotFoundError):
return None
if parsed:
return _stat[_stat.rfind(")") + 1:].split()
return _stat
get_thread_ids(self)
¶
Return a list of thread ids opened by process.
Source code in lutris/util/process.py
def get_thread_ids(self):
"""Return a list of thread ids opened by process."""
basedir = "/proc/{}/task/".format(self.pid)
if os.path.isdir(basedir):
try:
return os.listdir(basedir)
except FileNotFoundError:
return []
else:
return []
iter_children(self)
¶
Iterator that yields all the children of a process
Source code in lutris/util/process.py
def iter_children(self):
"""Iterator that yields all the children of a process"""
for child in self.children:
yield child
yield from child.iter_children()
wait_for_finish(self)
¶
Waits until the process finishes This only works if self.pid is a child process of Lutris
Source code in lutris/util/process.py
def wait_for_finish(self):
"""Waits until the process finishes
This only works if self.pid is a child process of Lutris
"""
try:
pid, ret_status = os.waitpid(int(self.pid) * -1, 0)
except OSError as ex:
logger.error("Failed to get exit status for PID %s", self.pid)
logger.error(ex)
return -1
logger.info("PID %s exited with code %s", pid, ret_status)
return ret_status
process_watcher
¶
Process management
SYSTEM_PROCESSES
¶
ProcessWatcher
¶
Keeps track of child processes of the client
Source code in lutris/util/process_watcher.py
class ProcessWatcher:
"""Keeps track of child processes of the client"""
def __init__(self, include_processes, exclude_processes):
"""Create a process watcher.
Params:
exclude_processes (str or list): list of processes that shouldn't be monitored
include_processes (str or list): list of process that should be forced to be monitored
"""
self.unmonitored_processes = (
self.parse_process_list(exclude_processes) | SYSTEM_PROCESSES
) - self.parse_process_list(include_processes)
@staticmethod
def parse_process_list(process_list):
"""Parse a process list that may be given as a string"""
if not process_list:
return set()
if isinstance(process_list, str):
process_list = shlex.split(process_list)
# process names from /proc only contain 15 characters
return {p[0:15] for p in process_list}
@staticmethod
def iterate_children():
"""Iterates through all children process of the lutris client.
This is not accurate since not all processes are started by
lutris but are started by Systemd instead.
"""
return Process(os.getpid()).iter_children()
def iterate_processes(self):
for child in self.iterate_children():
if child.state == 'Z':
continue
if child.name and child.name not in self.unmonitored_processes:
yield child
def is_alive(self, message=None):
"""Returns whether at least one watched process exists"""
if message:
sys.stdout.write("%s\n" % message)
return next(self.iterate_processes(), None) is not None
__init__(self, include_processes, exclude_processes)
special
¶
Create a process watcher.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exclude_processes |
str or list |
list of processes that shouldn't be monitored |
required |
include_processes |
str or list |
list of process that should be forced to be monitored |
required |
Source code in lutris/util/process_watcher.py
def __init__(self, include_processes, exclude_processes):
"""Create a process watcher.
Params:
exclude_processes (str or list): list of processes that shouldn't be monitored
include_processes (str or list): list of process that should be forced to be monitored
"""
self.unmonitored_processes = (
self.parse_process_list(exclude_processes) | SYSTEM_PROCESSES
) - self.parse_process_list(include_processes)
is_alive(self, message=None)
¶
Returns whether at least one watched process exists
Source code in lutris/util/process_watcher.py
def is_alive(self, message=None):
"""Returns whether at least one watched process exists"""
if message:
sys.stdout.write("%s\n" % message)
return next(self.iterate_processes(), None) is not None
iterate_children()
staticmethod
¶
Iterates through all children process of the lutris client. This is not accurate since not all processes are started by lutris but are started by Systemd instead.
Source code in lutris/util/process_watcher.py
@staticmethod
def iterate_children():
"""Iterates through all children process of the lutris client.
This is not accurate since not all processes are started by
lutris but are started by Systemd instead.
"""
return Process(os.getpid()).iter_children()
iterate_processes(self)
¶
Source code in lutris/util/process_watcher.py
def iterate_processes(self):
for child in self.iterate_children():
if child.state == 'Z':
continue
if child.name and child.name not in self.unmonitored_processes:
yield child
parse_process_list(process_list)
staticmethod
¶
Parse a process list that may be given as a string
Source code in lutris/util/process_watcher.py
@staticmethod
def parse_process_list(process_list):
"""Parse a process list that may be given as a string"""
if not process_list:
return set()
if isinstance(process_list, str):
process_list = shlex.split(process_list)
# process names from /proc only contain 15 characters
return {p[0:15] for p in process_list}
resources
¶
Utility module to handle media resources
get_icon_path(game_slug)
¶
Return the absolute path for a game_slug icon
Source code in lutris/util/resources.py
def get_icon_path(game_slug):
"""Return the absolute path for a game_slug icon"""
return os.path.join(settings.ICON_PATH, "lutris_%s.png" % game_slug)
settings
¶
SettingsIO
¶
ConfigParser abstraction.
Source code in lutris/util/settings.py
class SettingsIO:
"""ConfigParser abstraction."""
def __init__(self, config_file):
self.config_file = config_file
self.config = configparser.ConfigParser()
if os.path.exists(self.config_file):
try:
self.config.read([self.config_file])
except configparser.ParsingError as ex:
logger.error("Failed to readconfig file %s: %s", self.config_file, ex)
except UnicodeDecodeError as ex:
logger.error("Some invalid characters are preventing " "the setting file from loading properly: %s", ex)
def read_setting(self, key, section="lutris", default=""):
"""Read a setting from the config file
Params:
key (str): Setting key
section (str): Optional section, default to 'lutris'
default (str): Default value to return if setting not present
"""
try:
return self.config.get(section, key)
except (configparser.NoOptionError, configparser.NoSectionError):
return default
def write_setting(self, key, value, section="lutris"):
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, str(value))
with open(self.config_file, "w", encoding='utf-8') as config_file:
self.config.write(config_file)
__init__(self, config_file)
special
¶
Source code in lutris/util/settings.py
def __init__(self, config_file):
self.config_file = config_file
self.config = configparser.ConfigParser()
if os.path.exists(self.config_file):
try:
self.config.read([self.config_file])
except configparser.ParsingError as ex:
logger.error("Failed to readconfig file %s: %s", self.config_file, ex)
except UnicodeDecodeError as ex:
logger.error("Some invalid characters are preventing " "the setting file from loading properly: %s", ex)
read_setting(self, key, section='lutris', default='')
¶
Read a setting from the config file
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key |
str |
Setting key |
required |
section |
str |
Optional section, default to 'lutris' |
'lutris' |
default |
str |
Default value to return if setting not present |
'' |
Source code in lutris/util/settings.py
def read_setting(self, key, section="lutris", default=""):
"""Read a setting from the config file
Params:
key (str): Setting key
section (str): Optional section, default to 'lutris'
default (str): Default value to return if setting not present
"""
try:
return self.config.get(section, key)
except (configparser.NoOptionError, configparser.NoSectionError):
return default
write_setting(self, key, value, section='lutris')
¶
Source code in lutris/util/settings.py
def write_setting(self, key, value, section="lutris"):
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, str(value))
with open(self.config_file, "w", encoding='utf-8') as config_file:
self.config.write(config_file)
shell
¶
Controls execution of programs in separate shells
get_bash_rc_file(cwd, env, aliases=None)
¶
Return a bash prompt configured with pre-defined environment variables and aliases
Source code in lutris/util/shell.py
def get_bash_rc_file(cwd, env, aliases=None):
"""Return a bash prompt configured with pre-defined environment variables and aliases"""
script_path = os.path.join(settings.CACHE_DIR, "bashrc.sh")
env["TERM"] = "xterm"
exported_environment = "\n".join('export %s="%s"' % (key, value) for key, value in env.items())
aliases = aliases or {}
alias_commands = "\n".join('alias %s="%s"' % (key, value) for key, value in aliases.items())
current_bashrc = os.path.expanduser("~/.bashrc")
with open(script_path, "w", encoding='utf-8') as script_file:
script_file.write(
dedent(
"""
. %s
%s
%s
cd "%s"
""" % (
current_bashrc,
exported_environment,
alias_commands,
cwd,
)
)
)
return script_path
get_shell_command(cwd, env, aliases=None)
¶
Generates a scripts whichs opens a bash shell configured with given environment variables and aliases.
Source code in lutris/util/shell.py
def get_shell_command(cwd, env, aliases=None):
"""Generates a scripts whichs opens a bash shell configured with given
environment variables and aliases.
"""
bashrc_file = get_bash_rc_file(cwd, env, aliases)
return get_terminal_script(["bash", "--rcfile", bashrc_file], cwd, env)
get_terminal_script(command, cwd, env)
¶
Write command in a script file and run it.
Running it from a file is likely the only way to set env vars only for the command (not for the terminal app). It's also the only reliable way to keep the term open when the game is quit.
Source code in lutris/util/shell.py
def get_terminal_script(command, cwd, env):
"""Write command in a script file and run it.
Running it from a file is likely the only way to set env vars only
for the command (not for the terminal app).
It's also the only reliable way to keep the term open when the
game is quit.
"""
script_path = os.path.join(settings.CACHE_DIR, "run_in_term.sh")
env["TERM"] = "xterm"
exported_environment = "\n".join('export %s="%s" ' % (key, value) for key, value in env.items())
command = " ".join(['"%s"' % token for token in command])
with open(script_path, "w", encoding='utf-8') as script_file:
script_file.write(
dedent(
"""#!/bin/sh
cd "%s"
%s
exec %s
exit $?
""" % (cwd, exported_environment, command)
)
)
os.chmod(script_path, 0o744)
return script_path
steam
special
¶
appmanifest
¶
Steam appmanifest file handling
APP_STATE_FLAGS
¶
AppManifest
¶
Representation of an AppManifest file from Steam
Source code in lutris/util/steam/appmanifest.py
class AppManifest:
"""Representation of an AppManifest file from Steam"""
def __init__(self, appmanifest_path):
self.appmanifest_path = appmanifest_path
self.steamapps_path, filename = os.path.split(appmanifest_path)
self.steamid = re.findall(r"(\d+)", filename)[-1]
self.appmanifest_data = {}
if path_exists(appmanifest_path):
with open(appmanifest_path, "r", encoding='utf-8') as appmanifest_file:
self.appmanifest_data = vdf_parse(appmanifest_file, {})
else:
logger.error("Path to AppManifest file %s doesn't exist", appmanifest_path)
def __repr__(self):
return "<AppManifest: %s>" % self.appmanifest_path
@property
def app_state(self):
"""State of the app (dictionary containing game specific info)"""
return self.appmanifest_data.get("AppState") or {}
@property
def user_config(self):
"""Return the user configuration part"""
return self.app_state.get("UserConfig") or {}
@property
def name(self):
"""Return the game name from either the state or the user config"""
_name = self.app_state.get("name")
if not _name:
_name = self.user_config.get("name")
return _name
@property
def slug(self):
"""Return a slugified version of the name"""
return slugify(self.name)
@property
def installdir(self):
"""Path where the game is installed"""
return self.app_state.get("installdir")
@property
def states(self):
"""Return the states of a Steam game."""
states = []
state_flags = self.app_state.get("StateFlags", 0)
state_flags = bin(int(state_flags))[:1:-1]
for index, flag in enumerate(state_flags):
if flag == "1":
states.append(APP_STATE_FLAGS[index + 1])
return states
def is_installed(self):
"""True if the game is fully installed"""
return "Fully Installed" in self.states
def get_install_path(self):
"""Absolute path of the installation directory"""
if not self.installdir:
return None
install_path = fix_path_case(os.path.join(self.steamapps_path, "common", self.installdir))
if install_path and path_exists(install_path):
return install_path
return None
def get_platform(self):
"""Platform the game uses (linux or windows)"""
steamapps_paths = get_steamapps_paths()
if self.steamapps_path in steamapps_paths["linux"]:
return "linux"
if self.steamapps_path in steamapps_paths["windows"]:
return "windows"
raise ValueError("Can't find %s in %s" % (self.steamapps_path, steamapps_paths))
app_state
property
readonly
¶
State of the app (dictionary containing game specific info)
installdir
property
readonly
¶
Path where the game is installed
name
property
readonly
¶
Return the game name from either the state or the user config
slug
property
readonly
¶
Return a slugified version of the name
states
property
readonly
¶
Return the states of a Steam game.
user_config
property
readonly
¶
Return the user configuration part
__init__(self, appmanifest_path)
special
¶
Source code in lutris/util/steam/appmanifest.py
def __init__(self, appmanifest_path):
self.appmanifest_path = appmanifest_path
self.steamapps_path, filename = os.path.split(appmanifest_path)
self.steamid = re.findall(r"(\d+)", filename)[-1]
self.appmanifest_data = {}
if path_exists(appmanifest_path):
with open(appmanifest_path, "r", encoding='utf-8') as appmanifest_file:
self.appmanifest_data = vdf_parse(appmanifest_file, {})
else:
logger.error("Path to AppManifest file %s doesn't exist", appmanifest_path)
__repr__(self)
special
¶
Source code in lutris/util/steam/appmanifest.py
def __repr__(self):
return "<AppManifest: %s>" % self.appmanifest_path
get_install_path(self)
¶
Absolute path of the installation directory
Source code in lutris/util/steam/appmanifest.py
def get_install_path(self):
"""Absolute path of the installation directory"""
if not self.installdir:
return None
install_path = fix_path_case(os.path.join(self.steamapps_path, "common", self.installdir))
if install_path and path_exists(install_path):
return install_path
return None
get_platform(self)
¶
Platform the game uses (linux or windows)
Source code in lutris/util/steam/appmanifest.py
def get_platform(self):
"""Platform the game uses (linux or windows)"""
steamapps_paths = get_steamapps_paths()
if self.steamapps_path in steamapps_paths["linux"]:
return "linux"
if self.steamapps_path in steamapps_paths["windows"]:
return "windows"
raise ValueError("Can't find %s in %s" % (self.steamapps_path, steamapps_paths))
is_installed(self)
¶
True if the game is fully installed
Source code in lutris/util/steam/appmanifest.py
def is_installed(self):
"""True if the game is fully installed"""
return "Fully Installed" in self.states
get_appmanifest_from_appid(steamapps_path, appid)
¶
Given the steam apps path and appid, return the corresponding appmanifest
Source code in lutris/util/steam/appmanifest.py
def get_appmanifest_from_appid(steamapps_path, appid):
"""Given the steam apps path and appid, return the corresponding appmanifest"""
if not steamapps_path:
raise ValueError("steamapps_path is mandatory")
if not path_exists(steamapps_path):
raise IOError("steamapps_path must be a valid directory")
if not appid:
raise ValueError("Missing mandatory appid")
appmanifest_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
if not path_exists(appmanifest_path):
return None
return AppManifest(appmanifest_path)
get_appmanifests(steamapps_path)
¶
Return the list for all appmanifest files in a Steam library folder
Source code in lutris/util/steam/appmanifest.py
def get_appmanifests(steamapps_path):
"""Return the list for all appmanifest files in a Steam library folder"""
return [f for f in os.listdir(steamapps_path) if re.match(r"^appmanifest_\d+.acf$", f)]
get_path_from_appmanifest(steamapps_path, appid)
¶
Return the path where a Steam game is installed.
Source code in lutris/util/steam/appmanifest.py
def get_path_from_appmanifest(steamapps_path, appid):
"""Return the path where a Steam game is installed."""
appmanifest = get_appmanifest_from_appid(steamapps_path, appid)
if not appmanifest:
return None
return appmanifest.get_install_path()
config
¶
Handle Steam configuration
STEAM_DATA_DIRS
¶
get_config_value(config, key)
¶
Fetch a value from a configuration in a case insensitive way
Source code in lutris/util/steam/config.py
def get_config_value(config, key):
"""Fetch a value from a configuration in a case insensitive way"""
keymap = {k.lower(): k for k in config.keys()}
if key not in keymap:
logger.warning(
"Config key %s not found in %s", key, ", ".join(list(config.keys()))
)
return
return config[keymap[key.lower()]]
get_default_acf(appid, name)
¶
Return a default configuration usable to create a runnable game in Steam
Source code in lutris/util/steam/config.py
def get_default_acf(appid, name):
"""Return a default configuration usable to
create a runnable game in Steam"""
userconfig = OrderedDict()
userconfig["name"] = name
userconfig["gameid"] = appid
appstate = OrderedDict()
appstate["appID"] = appid
appstate["Universe"] = "1"
appstate["StateFlags"] = "1026"
appstate["installdir"] = name
appstate["UserConfig"] = userconfig
return {"AppState": appstate}
get_steam_dir()
¶
Main installation directory for Steam
Source code in lutris/util/steam/config.py
def get_steam_dir():
"""Main installation directory for Steam"""
steam_dir = search_in_steam_dirs("steamapps")
if steam_dir:
return steam_dir[:-len("steamapps")]
get_steam_library(steamid)
¶
Return the list of games owned by a SteamID
Source code in lutris/util/steam/config.py
def get_steam_library(steamid):
"""Return the list of games owned by a SteamID"""
if not steamid:
raise ValueError("Missing SteamID")
steam_games_url = (
"https://api.steampowered.com/"
"IPlayerService/GetOwnedGames/v0001/"
"?key={}&steamid={}&format=json&include_appinfo=1"
"&include_played_free_games=1".format(
settings.STEAM_API_KEY, steamid
)
)
response = requests.get(steam_games_url)
if response.status_code > 400:
logger.error("Invalid response from steam: %s", response)
return []
json_data = response.json()
response = json_data['response']
if not response:
logger.info("No games in response of %s", steam_games_url)
return []
if 'games' in response:
return response['games']
if 'game_count' in response and response['game_count'] == 0:
return []
logger.error("Weird response: %s", json_data)
return []
get_steamapps_paths()
¶
Source code in lutris/util/steam/config.py
def get_steamapps_paths():
from lutris.runners import steam # pylint: disable=import-outside-toplevel
return steam.steam().get_steamapps_dirs()
get_user_steam_id()
¶
Read user's SteamID from Steam config files
Source code in lutris/util/steam/config.py
def get_user_steam_id():
"""Read user's SteamID from Steam config files"""
user_config = read_user_config()
if not user_config or "users" not in user_config:
return
last_steam_id = None
for steam_id in user_config["users"]:
last_steam_id = steam_id
if get_config_value(user_config["users"][steam_id], "mostrecent") == "1":
return steam_id
return last_steam_id
read_config(steam_data_dir)
¶
Read the Steam configuration and return it as an object
Source code in lutris/util/steam/config.py
def read_config(steam_data_dir):
"""Read the Steam configuration and return it as an object"""
def get_entry_case_insensitive(config_dict, path):
for key, _value in config_dict.items():
if key.lower() == path[0].lower():
if len(path) <= 1:
return config_dict[key]
return get_entry_case_insensitive(config_dict[key], path[1:])
raise KeyError(path[0])
if not steam_data_dir:
return None
config_filename = os.path.join(steam_data_dir, "config/config.vdf")
if not system.path_exists(config_filename):
return None
with open(config_filename, "r", encoding='utf-8') as steam_config_file:
config = vdf_parse(steam_config_file, {})
try:
return get_entry_case_insensitive(config, ["InstallConfigStore", "Software", "Valve", "Steam"])
except KeyError as ex:
logger.error("Steam config %s is empty: %s", config_filename, ex)
read_library_folders(steam_data_dir)
¶
Read the Steam Library Folders config and return it as an object
Source code in lutris/util/steam/config.py
def read_library_folders(steam_data_dir):
"""Read the Steam Library Folders config and return it as an object"""
def get_entry_case_insensitive(library_dict, path):
for key, value in library_dict.items():
if key.lower() == path[0].lower():
if len(path) <= 1:
return value
return get_entry_case_insensitive(library_dict[key], path[1:])
raise KeyError(path[0])
if not steam_data_dir:
return None
library_filename = os.path.join(steam_data_dir, "config/libraryfolders.vdf")
if not system.path_exists(library_filename):
return None
with open(library_filename, "r", encoding='utf-8') as steam_library_file:
library = vdf_parse(steam_library_file, {})
# The contentstatsid key is unused and causes problems when looking for library paths.
library["libraryfolders"].pop("contentstatsid", None)
try:
return get_entry_case_insensitive(library, ["libraryfolders"])
except KeyError as ex:
logger.error("Steam libraryfolders %s is empty: %s", library_filename, ex)
read_user_config()
¶
Source code in lutris/util/steam/config.py
def read_user_config():
config_filename = search_in_steam_dirs("config/loginusers.vdf")
if not system.path_exists(config_filename):
return None
with open(config_filename, "r", encoding='utf-8') as steam_config_file:
config = vdf_parse(steam_config_file, {})
return config
search_in_steam_dirs(file)
¶
Find the (last) file/dir in all the Steam directories
Source code in lutris/util/steam/config.py
def search_in_steam_dirs(file):
"""Find the (last) file/dir in all the Steam directories"""
for candidate in STEAM_DATA_DIRS:
path = system.fix_path_case(
os.path.join(os.path.expanduser(candidate), file)
)
if path and system.path_exists(path):
return path
log
¶
Steam log handling
get_app_log(steam_data_dir, appid, start_time=None)
¶
Return all log entries related to appid from the latest Steam run.
:param start_time: Time tuple, log entries older than this are dumped.
Source code in lutris/util/steam/log.py
def get_app_log(steam_data_dir, appid, start_time=None):
"""Return all log entries related to appid from the latest Steam run.
:param start_time: Time tuple, log entries older than this are dumped.
"""
if start_time:
start_time = time.strftime("%Y-%m-%d %T", start_time)
app_log = []
for line in _get_last_content_log(steam_data_dir):
if start_time and line[1:20] < start_time:
continue
if " %s " % appid in line[22:]:
app_log.append(line)
return app_log
get_app_state_log(steam_data_dir, appid, start_time=None)
¶
Return state entries for appid from latest block in content_log.txt.
"Fully Installed, Running" means running. "Fully Installed" means stopped.
:param start_time: Time tuple, log entries older than this are dumped.
Source code in lutris/util/steam/log.py
def get_app_state_log(steam_data_dir, appid, start_time=None):
"""Return state entries for appid from latest block in content_log.txt.
"Fully Installed, Running" means running.
"Fully Installed" means stopped.
:param start_time: Time tuple, log entries older than this are dumped.
"""
state_log = []
for line in get_app_log(steam_data_dir, appid, start_time):
line = line.split(" : ")
if len(line) == 1:
continue
if line[0].endswith("state changed"):
state_log.append(line[1][:-2])
return state_log
vdf
¶
Read and write VDF files
to_vdf(dict_data, level=0)
¶
Convert a dictionnary to Steam config file format
Source code in lutris/util/steam/vdf.py
def to_vdf(dict_data, level=0):
"""Convert a dictionnary to Steam config file format"""
vdf_data = ""
for key in dict_data:
value = dict_data[key]
if isinstance(value, dict):
vdf_data += '%s"%s"\n' % ("\t" * level, key)
vdf_data += "%s{\n" % ("\t" * level)
vdf_data += to_vdf(value, level + 1)
vdf_data += "%s}\n" % ("\t" * level)
else:
vdf_data += '%s"%s"\t\t"%s"\n' % ("\t" * level, key, value)
return vdf_data
vdf_parse(steam_config_file, config)
¶
Parse a Steam config file and return the contents as a dict.
Source code in lutris/util/steam/vdf.py
def vdf_parse(steam_config_file, config):
"""Parse a Steam config file and return the contents as a dict."""
line = " "
while line:
try:
line = steam_config_file.readline()
except UnicodeDecodeError:
logger.error(
"Error while reading Steam VDF file %s. Returning %s",
steam_config_file,
config,
)
return config
if not line or line.strip() == "}":
return config
while not line.strip().endswith('"'):
nextline = steam_config_file.readline()
if not nextline:
break
line = line[:-1] + nextline
line_elements = line.strip().split('"')
if len(line_elements) == 3:
key = line_elements[1]
steam_config_file.readline() # skip '{'
config[key] = vdf_parse(steam_config_file, {})
else:
try:
config[line_elements[1]] = line_elements[3]
except IndexError:
logger.error("Malformed config file: %s", line)
return config
vdf_write(vdf_path, config)
¶
Write a Steam configuration to a vdf file
Source code in lutris/util/steam/vdf.py
def vdf_write(vdf_path, config):
"""Write a Steam configuration to a vdf file"""
vdf_data = to_vdf(config)
with open(vdf_path, "w", encoding='utf-8') as vdf_file:
vdf_file.write(vdf_data)
watcher
¶
Steam game library watcher
SteamWatcher
¶
Watches a Steam library folder and notify changes
Source code in lutris/util/steam/watcher.py
class SteamWatcher:
"""Watches a Steam library folder and notify changes"""
def __init__(self, steamapps_paths, callback=None):
self.monitors = []
self.callback = callback
for steam_path in steamapps_paths:
path = Gio.File.new_for_path(steam_path)
try:
monitor = path.monitor_directory(Gio.FileMonitorFlags.NONE)
logger.debug("Watching Steam folder %s", steam_path)
monitor.connect("changed", self._on_directory_changed)
self.monitors.append(monitor)
except GLib.Error as ex:
logger.exception(ex)
def _on_directory_changed(self, _monitor, _file, _other_file, event_type):
path = _file.get_path()
if not path.endswith(".acf"):
return
self.callback(event_type, path)
__init__(self, steamapps_paths, callback=None)
special
¶
Source code in lutris/util/steam/watcher.py
def __init__(self, steamapps_paths, callback=None):
self.monitors = []
self.callback = callback
for steam_path in steamapps_paths:
path = Gio.File.new_for_path(steam_path)
try:
monitor = path.monitor_directory(Gio.FileMonitorFlags.NONE)
logger.debug("Watching Steam folder %s", steam_path)
monitor.connect("changed", self._on_directory_changed)
self.monitors.append(monitor)
except GLib.Error as ex:
logger.exception(ex)
strings
¶
String utilities
NO_PLAYTIME
¶
add_url_tags(text)
¶
Surround URL with tags.
Source code in lutris/util/strings.py
def add_url_tags(text):
"""Surround URL with <a> tags."""
return re.sub(
r"(http[s]?://("
r"?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)",
r'<a href="\1">\1</a>',
text,
)
get_formatted_playtime(playtime)
¶
Return a human readable value of the play time
Source code in lutris/util/strings.py
def get_formatted_playtime(playtime):
"""Return a human readable value of the play time"""
if not playtime:
return NO_PLAYTIME
try:
playtime = float(playtime)
except ValueError:
logger.warning("Invalid playtime value '%s'", playtime)
return NO_PLAYTIME
hours = math.floor(playtime)
if hours:
hours_text = "%d hour%s" % (hours, "s" if hours > 1 else "")
else:
hours_text = ""
minutes = int((playtime - hours) * 60)
if minutes:
minutes_text = "%d minute%s" % (minutes, "s" if minutes > 1 else "")
else:
minutes_text = ""
formatted_time = " and ".join([text for text in (hours_text, minutes_text) if text])
if formatted_time:
return formatted_time
if playtime:
return "Less than a minute"
return NO_PLAYTIME
gtk_safe(string)
¶
Return a string ready to used in Gtk widgets
Source code in lutris/util/strings.py
def gtk_safe(string):
"""Return a string ready to used in Gtk widgets"""
if not string:
string = ""
string = str(string)
return string.replace("&", "&").replace("<", "<").replace(">", ">")
human_size(size)
¶
Shows a size in bytes in a more readable way
Source code in lutris/util/strings.py
def human_size(size):
"""Shows a size in bytes in a more readable way"""
units = ("bytes", "kB", "MB", "GB", "TB", "PB", "nuh uh", "no way", "BS")
unit_index = 0
while size > 1024:
size = size / 1024
unit_index += 1
return "%0.1f %s" % (size, units[unit_index])
lookup_string_in_text(string, text)
¶
Return full line if string found in the multi-line text.
Source code in lutris/util/strings.py
def lookup_string_in_text(string, text):
"""Return full line if string found in the multi-line text."""
output_lines = text.split("\n")
for line in output_lines:
if string in line:
return line
parse_version(version)
¶
Parse a version string
Return a 3 element tuple containing: - The version number as a list of integers - The prefix (whatever characters before the version number) - The suffix (whatever comes after)
Example:: >>> parse_version("3.6-staging") ([3, 6], '', '-staging')
Returns:
| Type | Description |
|---|---|
tuple |
(version number as list, prefix, suffix) |
Source code in lutris/util/strings.py
def parse_version(version):
"""Parse a version string
Return a 3 element tuple containing:
- The version number as a list of integers
- The prefix (whatever characters before the version number)
- The suffix (whatever comes after)
Example::
>>> parse_version("3.6-staging")
([3, 6], '', '-staging')
Returns:
tuple: (version number as list, prefix, suffix)
"""
version_match = re.search(r"(\d[\d\.]+\d)", version)
if not version_match:
return [], "", ""
version_number = version_match.groups()[0]
prefix = version[0:version_match.span()[0]]
suffix = version[version_match.span()[1]:]
return [int(p) for p in version_number.split(".")], suffix, prefix
slugify(value)
¶
Remove special characters from a string and slugify it.
Normalizes string, converts to lowercase, removes non-alpha characters, and converts spaces to hyphens.
Source code in lutris/util/strings.py
def slugify(value):
"""Remove special characters from a string and slugify it.
Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens.
"""
_value = str(value)
# This differs from the Lutris website implementation which uses the Django
# version of `slugify` and uses the "NFKD" normalization method instead of
# "NFD". This creates some inconsistencies in titles containing a trademark
# symbols or some other special characters. The website version of slugify
# will likely get updated to use the same normalization method.
_value = unicodedata.normalize("NFD", _value).encode("ascii", "ignore")
_value = _value.decode("utf-8")
_value = str(re.sub(r"[^\w\s-]", "", _value)).strip().lower()
slug = re.sub(r"[-\s]+", "-", _value)
if not slug:
# The slug is empty, likely because the string contains only non-latin
# characters
slug = str(uuid.uuid5(uuid.NAMESPACE_URL, str(value)))
return slug
split_arguments(args)
¶
Wrapper around shlex.split that is more tolerant of errors
Source code in lutris/util/strings.py
def split_arguments(args):
"""Wrapper around shlex.split that is more tolerant of errors"""
if not args:
# shlex.split seems to hangs when passed the None value
return []
return _split_arguments(args)
unpack_dependencies(string)
¶
Parse a string to allow for complex dependencies Works in a similar fashion as Debian dependencies, separate dependencies are comma separated and multiple choices for satisfying a dependency are separated by pipes.
quake-steam | quake-gog, some-quake-mod returns:
[('quake-steam', 'quake-gog'), 'some-quake-mod']
Source code in lutris/util/strings.py
def unpack_dependencies(string):
"""Parse a string to allow for complex dependencies
Works in a similar fashion as Debian dependencies, separate dependencies
are comma separated and multiple choices for satisfying a dependency are
separated by pipes.
Example: quake-steam | quake-gog, some-quake-mod returns:
[('quake-steam', 'quake-gog'), 'some-quake-mod']
"""
if not string:
return []
dependencies = [dep.strip() for dep in string.split(",") if dep.strip()]
for index, dependency in enumerate(dependencies):
if "|" in dependency:
dependencies[index] = tuple(option.strip() for option in dependency.split("|") if option.strip())
return [dependency for dependency in dependencies if dependency]
version_sort(versions, reverse=False)
¶
Source code in lutris/util/strings.py
def version_sort(versions, reverse=False):
def version_key(version):
version_list, prefix, suffix = parse_version(version)
# Normalize the length of sub-versions
sort_key = version_list + [0] * (10 - len(version_list))
sort_key.append(prefix)
sort_key.append(suffix)
return sort_key
return sorted(versions, key=version_key, reverse=reverse)
system
¶
System utilities
PROTECTED_HOME_FOLDERS
¶
create_folder(path)
¶
Creates a folder specified by path
Source code in lutris/util/system.py
def create_folder(path):
"""Creates a folder specified by path"""
if not path:
return
path = os.path.expanduser(path)
os.makedirs(path, exist_ok=True)
return path
execute(command, env=None, cwd=None, log_errors=False, quiet=False, shell=False, timeout=None)
¶
Execute a system command and return its results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
command |
list |
A list containing an executable and its parameters |
required |
env |
dict |
Dict of values to add to the current environment |
None |
cwd |
str |
Working directory |
None |
log_errors |
bool |
Pipe stderr to stdout (might cause slowdowns) |
False |
quiet |
bool |
Do not display log messages |
False |
timeout |
int |
Number of seconds the program is allowed to run, disabled by default |
None |
Returns:
| Type | Description |
|---|---|
str |
stdout output |
Source code in lutris/util/system.py
def execute(command, env=None, cwd=None, log_errors=False, quiet=False, shell=False, timeout=None):
"""
Execute a system command and return its results.
Params:
command (list): A list containing an executable and its parameters
env (dict): Dict of values to add to the current environment
cwd (str): Working directory
log_errors (bool): Pipe stderr to stdout (might cause slowdowns)
quiet (bool): Do not display log messages
timeout (int): Number of seconds the program is allowed to run, disabled by default
Returns:
str: stdout output
"""
# Check if the executable exists
if not command:
logger.error("No executable provided!")
return ""
if os.path.isabs(command[0]) and not path_exists(command[0]):
logger.error("No executable found in %s", command)
return ""
if not quiet:
logger.debug("Executing %s", " ".join([str(i) for i in command]))
# Set up environment
existing_env = os.environ.copy()
if env:
if not quiet:
logger.debug(" ".join("{}={}".format(k, v) for k, v in env.items()))
env = {k: v for k, v in env.items() if v is not None}
existing_env.update(env)
# Piping stderr can cause slowness in the programs, use carefully
# (especially when using regedit with wine)
try:
with subprocess.Popen(
command,
shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE if log_errors else subprocess.DEVNULL,
env=existing_env,
cwd=cwd,
errors="replace"
) as command_process:
stdout, stderr = command_process.communicate(timeout=timeout)
except (OSError, TypeError) as ex:
logger.error("Could not run command %s (env: %s): %s", command, env, ex)
return ""
except subprocess.TimeoutExpired:
logger.error("Command %s after %s seconds", command, timeout)
return ""
if stderr and log_errors:
logger.error(stderr)
return stdout.strip()
find_executable(exec_name)
¶
Return the absolute path of an executable
Source code in lutris/util/system.py
def find_executable(exec_name):
"""Return the absolute path of an executable"""
if not exec_name:
return None
return shutil.which(exec_name)
find_mount_point(path)
¶
Return the mount point a file is located on
Source code in lutris/util/system.py
def find_mount_point(path):
"""Return the mount point a file is located on"""
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
return path
fix_path_case(path)
¶
Do a case insensitive check, return the real path with correct case. If the path is not for a real file, this corrects as many components as do exist.
Source code in lutris/util/system.py
def fix_path_case(path):
"""Do a case insensitive check, return the real path with correct case. If the path is
not for a real file, this corrects as many components as do exist."""
if not path or os.path.exists(path):
# If a path isn't provided or it exists as is, return it.
return path
parts = path.strip("/").split("/")
current_path = "/"
for part in parts:
parent_path = current_path
current_path = os.path.join(current_path, part)
if not os.path.exists(current_path) and os.path.isdir(parent_path):
try:
path_contents = os.listdir(parent_path)
except OSError:
logger.error("Can't read contents of %s", parent_path)
path_contents = []
for filename in path_contents:
if filename.lower() == part.lower():
current_path = os.path.join(parent_path, filename)
break
# Only return the path if we got the same number of elements
if len(parts) == len(current_path.strip("/").split("/")):
return current_path
get_disk_size(path)
¶
Return the disk size in bytes of a folder
Source code in lutris/util/system.py
def get_disk_size(path):
"""Return the disk size in bytes of a folder"""
total_size = 0
for base, _dirs, files in os.walk(path):
total_size += sum([
os.stat(os.path.join(base, f)).st_size
for f in files
if os.path.isfile(os.path.join(base, f))
])
return total_size
get_drive_for_path(path)
¶
Return the physical drive a file is located on
Source code in lutris/util/system.py
def get_drive_for_path(path):
"""Return the physical drive a file is located on"""
return get_mountpoint_drives().get(find_mount_point(path))
get_existing_parent(path)
¶
Return the 1st existing parent for a folder (or itself if the path exists and is a directory). returns None, when none of the parents exists.
Source code in lutris/util/system.py
def get_existing_parent(path):
"""Return the 1st existing parent for a folder (or itself if the path
exists and is a directory). returns None, when none of the parents exists.
"""
if path == "":
return None
if os.path.exists(path) and not os.path.isfile(path):
return path
return get_existing_parent(os.path.dirname(path))
get_file_checksum(filename, hash_type)
¶
Return the checksum of type hash_type for a given filename
Source code in lutris/util/system.py
def get_file_checksum(filename, hash_type):
"""Return the checksum of type `hash_type` for a given filename"""
hasher = hashlib.new(hash_type)
with open(filename, "rb") as input_file:
for chunk in iter(lambda: input_file.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
get_md5_hash(filename)
¶
Return the md5 hash of a file.
Source code in lutris/util/system.py
def get_md5_hash(filename):
"""Return the md5 hash of a file."""
md5 = hashlib.md5()
try:
with open(filename, "rb") as _file:
for chunk in iter(lambda: _file.read(8192), b""):
md5.update(chunk)
except IOError:
logger.warning("Error reading %s", filename)
return False
return md5.hexdigest()
get_mounted_discs()
¶
Return a list of mounted discs and ISOs
:rtype: list of Gio.Mount
Source code in lutris/util/system.py
def get_mounted_discs():
"""Return a list of mounted discs and ISOs
:rtype: list of Gio.Mount
"""
volumes = Gio.VolumeMonitor.get()
drives = []
for mount in volumes.get_mounts():
if mount.get_volume():
device = mount.get_volume().get_identifier("unix-device")
if not device:
logger.debug("No device for mount %s", mount.get_name())
continue
# Device is a disk drive or ISO image
if "/dev/sr" in device or "/dev/loop" in device:
drives.append(mount.get_root().get_path())
return drives
get_mountpoint_drives()
¶
Return a mapping of mount points with their corresponding drives
Source code in lutris/util/system.py
def get_mountpoint_drives():
"""Return a mapping of mount points with their corresponding drives"""
mounts = read_process_output(["mount", "-v"]).split("\n")
mount_map = []
for mount in mounts:
mount_parts = mount.split()
if len(mount_parts) < 3:
continue
mount_map.append((mount_parts[2], mount_parts[0]))
return dict(mount_map)
get_pid(program, multiple=False)
¶
Return pid of process.
:param str program: Name of the process. :param bool multiple: If True and multiple instances of the program exist, return all of them; if False only return the first one.
Source code in lutris/util/system.py
def get_pid(program, multiple=False):
"""Return pid of process.
:param str program: Name of the process.
:param bool multiple: If True and multiple instances of the program exist,
return all of them; if False only return the first one.
"""
pids = execute(["pgrep", program])
if not pids.strip():
return
pids = pids.split()
if multiple:
return pids
return pids[0]
get_pids_using_file(path)
¶
Return a set of pids using file path.
Source code in lutris/util/system.py
def get_pids_using_file(path):
"""Return a set of pids using file `path`."""
if not os.path.exists(path):
logger.error("Can't return PIDs using non existing file: %s", path)
return set()
fuser_path = find_executable("fuser")
if not fuser_path:
logger.warning("fuser not available, please install psmisc")
return set([])
fuser_output = execute([fuser_path, path], quiet=True)
return set(fuser_output.split())
get_running_pid_list()
¶
Return the list of PIDs from processes currently running
Source code in lutris/util/system.py
def get_running_pid_list():
"""Return the list of PIDs from processes currently running"""
return [int(p) for p in os.listdir("/proc") if p[0].isdigit()]
is_executable(exec_path)
¶
Return whether exec_path is an executable
Source code in lutris/util/system.py
def is_executable(exec_path):
"""Return whether exec_path is an executable"""
return os.access(exec_path, os.X_OK)
is_removeable(path)
¶
Check if a folder is safe to remove (not system or home, ...)
Source code in lutris/util/system.py
def is_removeable(path):
"""Check if a folder is safe to remove (not system or home, ...)"""
if not path_exists(path):
return False
parts = path.strip("/").split("/")
if parts[0] in ("usr", "var", "lib", "etc", "boot", "sbin", "bin"):
# Path is part of the system folders
return False
if parts[0] == "home":
if len(parts) <= 2:
return False
if len(parts) == 3 and parts[2] in PROTECTED_HOME_FOLDERS:
return False
return True
kill_pid(pid)
¶
Terminate a process referenced by its PID
Source code in lutris/util/system.py
def kill_pid(pid):
"""Terminate a process referenced by its PID"""
try:
pid = int(pid)
except ValueError:
logger.error("Invalid pid %s")
return
logger.info("Killing PID %s", pid)
try:
os.kill(pid, signal.SIGKILL)
except OSError:
logger.error("Could not kill process %s", pid)
list_unique_folders(folders)
¶
Deduplicate directories with the same Device.Inode
Source code in lutris/util/system.py
def list_unique_folders(folders):
"""Deduplicate directories with the same Device.Inode"""
unique_dirs = {}
for folder in folders:
folder_stat = os.stat(folder)
identifier = "%s.%s" % (folder_stat.st_dev, folder_stat.st_ino)
if identifier not in unique_dirs:
unique_dirs[identifier] = folder
return unique_dirs.values()
make_executable(exec_path)
¶
Source code in lutris/util/system.py
def make_executable(exec_path):
file_stats = os.stat(exec_path)
os.chmod(exec_path, file_stats.st_mode | stat.S_IEXEC)
merge_folders(source, destination)
¶
Merges the content of source to destination
Source code in lutris/util/system.py
def merge_folders(source, destination):
"""Merges the content of source to destination"""
logger.debug("Merging %s into %s", source, destination)
# Check if dirs_exist_ok is defined ( Python >= 3.8)
sig = inspect.signature(shutil.copytree)
if "dirs_exist_ok" in sig.parameters:
shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True, dirs_exist_ok=True)
else:
shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True)
path_exists(path, check_symlinks=False, exclude_empty=False)
¶
Wrapper around system.path_exists that doesn't crash with empty values
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path |
str |
File to the file to check |
required |
check_symlinks |
bool |
If the path is a broken symlink, return False |
False |
exclude_empty |
bool |
If true, consider 0 bytes files as non existing |
False |
Source code in lutris/util/system.py
def path_exists(path, check_symlinks=False, exclude_empty=False):
"""Wrapper around system.path_exists that doesn't crash with empty values
Params:
path (str): File to the file to check
check_symlinks (bool): If the path is a broken symlink, return False
exclude_empty (bool): If true, consider 0 bytes files as non existing
"""
if not path:
return False
if os.path.exists(path):
if exclude_empty:
return os.stat(path).st_size > 0
return True
if os.path.islink(path):
logger.warning("%s is a broken link", path)
return not check_symlinks
return False
python_identifier(unsafe_string)
¶
Converts a string to something that can be used as a python variable
Source code in lutris/util/system.py
def python_identifier(unsafe_string):
"""Converts a string to something that can be used as a python variable"""
if not isinstance(unsafe_string, str):
logger.error("Cannot convert %s to a python identifier", type(unsafe_string))
return
def _dashrepl(matchobj):
return matchobj.group(0).replace("-", "_")
return re.sub(r"(\${)([\w-]*)(})", _dashrepl, unsafe_string)
read_process_output(command, timeout=5)
¶
Return the output of a command as a string
Source code in lutris/util/system.py
def read_process_output(command, timeout=5):
"""Return the output of a command as a string"""
try:
return subprocess.check_output(
command,
timeout=timeout,
encoding="utf-8",
errors="ignore"
).strip()
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex:
logger.error("%s command failed: %s", command, ex)
return ""
remove_folder(path)
¶
Delete a folder specified by path Returns true if the folder was successfully removed.
Source code in lutris/util/system.py
def remove_folder(path):
"""Delete a folder specified by path
Returns true if the folder was successfully removed.
"""
if not os.path.exists(path):
logger.warning("Non existent path: %s", path)
return
logger.debug("Removing folder %s", path)
if os.path.samefile(os.path.expanduser("~"), path):
raise RuntimeError("Lutris tried to erase home directory!")
try:
shutil.rmtree(path)
except OSError as ex:
logger.error("Failed to remove folder %s: %s (Error code %s)", path, ex.strerror, ex.errno)
return False
return True
reset_library_preloads()
¶
Remove library preloads from environment
Source code in lutris/util/system.py
def reset_library_preloads():
"""Remove library preloads from environment"""
for key in ("LD_LIBRARY_PATH", "LD_PRELOAD"):
if os.environ.get(key):
try:
del os.environ[key]
except OSError:
logger.error("Failed to delete environment variable %s", key)
reverse_expanduser(path)
¶
Replace '/home/username' with '~' in given path.
Source code in lutris/util/system.py
def reverse_expanduser(path):
"""Replace '/home/username' with '~' in given path."""
if not path:
return path
user_path = os.path.expanduser("~")
if path.startswith(user_path):
path = path[len(user_path):].strip("/")
return "~/" + path
return path
substitute(string_template, variables)
¶
Expand variables on a string template
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
string_template |
str |
template with variables preceded by $ |
required |
variables |
dict |
mapping of variable identifier > value |
required |
Returns:
| Type | Description |
|---|---|
str |
String with substituted values |
Source code in lutris/util/system.py
def substitute(string_template, variables):
"""Expand variables on a string template
Args:
string_template (str): template with variables preceded by $
variables (dict): mapping of variable identifier > value
Return:
str: String with substituted values
"""
string_template = python_identifier(str(string_template))
identifiers = variables.keys()
# We support dashes in identifiers but they are not valid in python
# identifiers, which is a requirement for the templating engine we use
# Replace the dashes with underscores in the mapping and template
variables = dict((k.replace("-", "_"), v) for k, v in variables.items())
for identifier in identifiers:
string_template = string_template.replace("${}".format(identifier), "${}".format(identifier.replace("-", "_")))
template = string.Template(string_template)
if string_template in list(variables.keys()):
return variables[string_template]
return template.safe_substitute(variables)
update_desktop_icons()
¶
Update Icon for GTK+ desktop manager Other desktop manager icon cache commands must be added here if needed
Source code in lutris/util/system.py
def update_desktop_icons():
"""Update Icon for GTK+ desktop manager
Other desktop manager icon cache commands must be added here if needed
"""
if find_executable("gtk-update-icon-cache"):
execute(["gtk-update-icon-cache", "-tf", os.path.join(GLib.get_user_data_dir(), "icons/hicolor")], quiet=True)
execute(["gtk-update-icon-cache", "-tf", os.path.join(settings.RUNTIME_DIR, "icons/hicolor")], quiet=True)
test_config
¶
setup_test_environment()
¶
Sets up a system to be able to run tests
Source code in lutris/util/test_config.py
def setup_test_environment():
"""Sets up a system to be able to run tests"""
os.environ["LUTRIS_SKIP_INIT"] = "1"
schema.syncdb()
startup.init_lutris()
timer
¶
Timer module
Timer
¶
Simple Timer class to time code
Source code in lutris/util/timer.py
class Timer:
"""Simple Timer class to time code"""
def __init__(self):
self._start = None
self._end = None
self.finished = False
def start(self):
"""Starts the timer"""
self._end = None
self._start = datetime.datetime.now()
self.finished = False
def end(self):
"""Ends the timer"""
self._end = datetime.datetime.now()
self.finished = True
@property
def duration(self):
"""Return the total duration of the timer"""
if not self._start:
return 0
if not self.finished:
_duration = (datetime.datetime.now() - self._start).seconds
else:
_duration = (self._end - self._start).seconds
return _duration
duration
property
readonly
¶
Return the total duration of the timer
__init__(self)
special
¶
Source code in lutris/util/timer.py
def __init__(self):
self._start = None
self._end = None
self.finished = False
end(self)
¶
Ends the timer
Source code in lutris/util/timer.py
def end(self):
"""Ends the timer"""
self._end = datetime.datetime.now()
self.finished = True
start(self)
¶
Starts the timer
Source code in lutris/util/timer.py
def start(self):
"""Starts the timer"""
self._end = None
self._start = datetime.datetime.now()
self.finished = False
update_cache
¶
Manage a cache file of execution times for updates
DATE_FORMAT
¶
UPDATE_CACHE_PATH
¶
get_last_call(key)
¶
Return the time in second since the last update for 'key' was made
Source code in lutris/util/update_cache.py
def get_last_call(key):
"""Return the time in second since the last update for 'key' was made"""
date = read_date_from_cache(key)
if not date:
return 0
delta = datetime.now() - date
return delta.seconds
read_date_from_cache(key)
¶
Return a datetime object from 'key'
Source code in lutris/util/update_cache.py
def read_date_from_cache(key):
"""Return a datetime object from 'key'"""
cache = _read_cache_content()
date = cache.get(key)
if not date:
return
date = datetime.strptime(date, DATE_FORMAT)
return date
write_date_to_cache(key)
¶
Write current time to the cache for 'key'
Source code in lutris/util/update_cache.py
def write_date_to_cache(key):
"""Write current time to the cache for 'key'"""
cache = _read_cache_content()
cache[key] = datetime.strftime(datetime.now(), DATE_FORMAT)
with open(UPDATE_CACHE_PATH, "w", encoding='utf-8') as json_file:
json.dump(cache, json_file, indent=2)
urlhandler
¶
Unused handler registration but since someone reports problems with URL integration once in a while, it could prove itself useful.
register_url_handler()
¶
Register the lutris: protocol to open with the application.
Source code in lutris/util/urlhandler.py
def register_url_handler():
"""Register the lutris: protocol to open with the application."""
executable = os.path.abspath(sys.argv[0])
base_key = "desktop.gnome.url-handlers.lutris"
schema_directory = "/usr/share/glib-2.0/schemas/"
schema_source = Gio.SettingsSchemaSource.new_from_directory(schema_directory, None, True)
schema = schema_source.lookup(base_key, True)
if schema:
settings = Gio.Settings.new(base_key)
settings.set_string("command", executable)
else:
logger.warning("Schema not installed, cannot register url-handler")
wine
special
¶
cabinstall
¶
CabInstaller
¶
Extract and install contents of cab files
Based on an implementation by tonix64: https://github.com/tonix64/python-installcab
Source code in lutris/util/wine/cabinstall.py
class CabInstaller:
"""Extract and install contents of cab files
Based on an implementation by tonix64: https://github.com/tonix64/python-installcab
"""
def __init__(self, prefix, arch=None, wine_path=None):
self.prefix = prefix
self.winearch = arch or self.get_wineprefix_arch()
self.tmpdir = tempfile.mkdtemp()
self.wine_path = wine_path
self.register_dlls = False # Whether to register DLLs, I don't the purpose of that
self.strip_dlls = False # When registering, strip the full path
@staticmethod
def process_key(key):
"""I have no clue why"""
return key.strip("\\").replace("HKEY_CLASSES_ROOT", "HKEY_LOCAL_MACHINE\\Software\\Classes")
@staticmethod
def get_arch_from_manifest(root):
registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}assemblyIdentity")
arch = registry_keys[0].attrib["processorArchitecture"]
arch_map = {"amd64": "win64", "x86": "win32", "wow64": "wow64"}
return arch_map[arch]
def get_winebin(self, arch):
wine_path = self.wine_path or "wine"
return wine_path if arch in ("win32", "wow64") else wine_path + "64"
@staticmethod
def get_arch_from_dll(dll_path):
if "x86-64" in read_process_output(["file", dll_path]):
return "win64"
return "win32"
def cleanup(self):
logger.info("Cleaning up %s", self.tmpdir)
shutil.rmtree(self.tmpdir)
def check_dll_arch(self, dll_path):
return self.get_arch_from_dll(dll_path)
def replace_variables(self, value, arch):
if "$(" in value:
value = value.replace("$(runtime.help)", "C:\\windows\\help")
value = value.replace("$(runtime.inf)", "C:\\windows\\inf")
value = value.replace("$(runtime.wbem)", "C:\\windows\\wbem")
value = value.replace("$(runtime.windows)", "C:\\windows")
value = value.replace("$(runtime.ProgramFiles)", "C:\\windows\\Program Files")
value = value.replace("$(runtime.programFiles)", "C:\\windows\\Program Files")
value = value.replace("$(runtime.programFilesX86)", "C:\\windows\\Program Files (x86)")
value = value.replace("$(runtime.system32)", "C:\\windows\\%s" % self.get_system32_realdir(arch))
value = value.replace(
"$(runtime.drivers)",
"C:\\windows\\%s\\drivers" % self.get_system32_realdir(arch),
)
value = value.replace("\\", "\\\\")
return value
def process_value(self, reg_value, arch):
attrs = reg_value.attrib
name = attrs["name"]
value = attrs["value"]
value_type = attrs["valueType"]
if not name.strip():
name = "@"
else:
name = '"%s"' % name
name = self.replace_variables(name, arch)
if value_type == "REG_BINARY":
value = re.findall("..", value)
value = "hex:" + ",".join(value)
elif value_type == "REG_DWORD":
value = "dword:%s" % value.replace("0x", "")
elif value_type == "REG_QWORD":
value = "qword:%s" % value.replace("0x", "")
elif value_type == "REG_NONE":
value = None
elif value_type == "REG_EXPAND_SZ":
# not sure if we should replace this ones at this point:
# caps can vary in the pattern
value = value.replace("%SystemRoot%", "C:\\windows")
value = value.replace("%ProgramFiles%", "C:\\windows\\Program Files")
value = value.replace("%WinDir%", "C:\\windows")
value = value.replace("%ResourceDir%", "C:\\windows")
value = value.replace("%Public%", "C:\\users\\Public")
value = value.replace("%LocalAppData%", "C:\\windows\\Public\\Local Settings\\Application Data")
value = value.replace("%AllUsersProfile%", "C:\\windows")
value = value.replace("%UserProfile%", "C:\\windows")
value = value.replace("%ProgramData%", "C:\\ProgramData")
value = '"%s"' % value
elif value_type == "REG_SZ":
value = '"%s"' % value
else:
logger.warning("warning unkown type: %s", value_type)
value = '"%s"' % value
if value:
value = self.replace_variables(value, arch)
if self.strip_dlls:
if ".dll" in value:
value = value.lower().replace("c:\\\\windows\\\\system32\\\\", "")
value = value.lower().replace("c:\\\\windows\\\\syswow64\\\\", "")
return name, value
def get_registry_from_manifest(self, file_name):
out = ""
root = xml.etree.ElementTree.parse(file_name).getroot()
arch = self.get_arch_from_manifest(root)
registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}registryKeys")
if registry_keys:
for registry_key in list(registry_keys[0]):
key = self.process_key(registry_key.attrib["keyName"])
out += "[%s]\n" % key
for reg_value in registry_key.findall("{urn:schemas-microsoft-com:asm.v3}registryValue"):
name, value = self.process_value(reg_value, arch)
if value is not None:
out += "%s=%s\n" % (name, value)
out += "\n"
return (out, arch)
def get_wineprefix_arch(self):
with open(os.path.join(self.prefix, "system.reg"), encoding='utf-8') as reg_file:
for line in reg_file.readlines():
if line.startswith("#arch=win32"):
return "win32"
if line.startswith("#arch=win64"):
return "win64"
return "win64"
def get_system32_realdir(self, arch):
dest_map = {
("win64", "win32"): "Syswow64",
("win64", "win64"): "System32",
("win64", "wow64"): "System32",
("win32", "win32"): "System32",
}
return dest_map[(self.winearch, arch)]
def get_dll_destdir(self, dll_path):
if self.get_arch_from_dll(dll_path) == "win32" and self.winearch == "win64":
return os.path.join(self.prefix, "drive_c/windows/syswow64")
return os.path.join(self.prefix, "drive_c/windows/system32")
def install_dll(self, dll_path):
dest_dir = self.get_dll_destdir(dll_path)
logger.debug("Copying %s to %s", dll_path, dest_dir)
shutil.copy(dll_path, dest_dir)
dest_dll_path = os.path.join(dest_dir, os.path.basename(dll_path))
if not self.register_dlls:
return
arch = self.get_arch_from_dll(dest_dll_path)
subprocess.call([self.get_winebin(arch), "regsvr32", os.path.basename(dest_dll_path)])
def get_registry_files(self, output_files):
reg_files = []
for file_path in output_files:
if file_path.endswith(".manifest"):
out = "Windows Registry Editor Version 5.00\n\n"
outdata, arch = self.get_registry_from_manifest(file_path)
if outdata:
out += outdata
with open(os.path.join(self.tmpdir, file_path + ".reg"), "w", encoding='utf-8') as reg_file:
reg_file.write(out)
reg_files.append((file_path + ".reg", arch))
if file_path.endswith(".dll"):
self.install_dll(file_path)
return reg_files
def apply_to_registry(self, file_path, arch):
logger.info("Applying %s to registry", file_path)
subprocess.call([self.get_winebin(arch), "regedit", os.path.join(self.tmpdir, file_path)])
def extract_from_cab(self, cabfile, component):
"""Extracts files matching a `component` name from a `cabfile`
Params:
cabfile (str): Path to a cabfile to extract from
component (str): component to extract from the cab file
Returns:
list: Files extracted from the cab file
"""
execute(["cabextract", "-F", "*%s*" % component, "-d", self.tmpdir, cabfile])
return [os.path.join(r, file) for r, d, f in os.walk(self.tmpdir) for file in f]
def install(self, cabfile, component):
"""Install `component` from `cabfile`"""
logger.info("Installing %s from %s", component, cabfile)
for file_path, arch in self.get_registry_files(self.extract_from_cab(cabfile, component)):
self.apply_to_registry(file_path, arch)
self.cleanup()
__init__(self, prefix, arch=None, wine_path=None)
special
¶
Source code in lutris/util/wine/cabinstall.py
def __init__(self, prefix, arch=None, wine_path=None):
self.prefix = prefix
self.winearch = arch or self.get_wineprefix_arch()
self.tmpdir = tempfile.mkdtemp()
self.wine_path = wine_path
self.register_dlls = False # Whether to register DLLs, I don't the purpose of that
self.strip_dlls = False # When registering, strip the full path
apply_to_registry(self, file_path, arch)
¶
Source code in lutris/util/wine/cabinstall.py
def apply_to_registry(self, file_path, arch):
logger.info("Applying %s to registry", file_path)
subprocess.call([self.get_winebin(arch), "regedit", os.path.join(self.tmpdir, file_path)])
check_dll_arch(self, dll_path)
¶
Source code in lutris/util/wine/cabinstall.py
def check_dll_arch(self, dll_path):
return self.get_arch_from_dll(dll_path)
cleanup(self)
¶
Source code in lutris/util/wine/cabinstall.py
def cleanup(self):
logger.info("Cleaning up %s", self.tmpdir)
shutil.rmtree(self.tmpdir)
extract_from_cab(self, cabfile, component)
¶
Extracts files matching a component name from a cabfile
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cabfile |
str |
Path to a cabfile to extract from |
required |
component |
str |
component to extract from the cab file |
required |
Returns:
| Type | Description |
|---|---|
list |
Files extracted from the cab file |
Source code in lutris/util/wine/cabinstall.py
def extract_from_cab(self, cabfile, component):
"""Extracts files matching a `component` name from a `cabfile`
Params:
cabfile (str): Path to a cabfile to extract from
component (str): component to extract from the cab file
Returns:
list: Files extracted from the cab file
"""
execute(["cabextract", "-F", "*%s*" % component, "-d", self.tmpdir, cabfile])
return [os.path.join(r, file) for r, d, f in os.walk(self.tmpdir) for file in f]
get_arch_from_dll(dll_path)
staticmethod
¶
Source code in lutris/util/wine/cabinstall.py
@staticmethod
def get_arch_from_dll(dll_path):
if "x86-64" in read_process_output(["file", dll_path]):
return "win64"
return "win32"
get_arch_from_manifest(root)
staticmethod
¶
Source code in lutris/util/wine/cabinstall.py
@staticmethod
def get_arch_from_manifest(root):
registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}assemblyIdentity")
arch = registry_keys[0].attrib["processorArchitecture"]
arch_map = {"amd64": "win64", "x86": "win32", "wow64": "wow64"}
return arch_map[arch]
get_dll_destdir(self, dll_path)
¶
Source code in lutris/util/wine/cabinstall.py
def get_dll_destdir(self, dll_path):
if self.get_arch_from_dll(dll_path) == "win32" and self.winearch == "win64":
return os.path.join(self.prefix, "drive_c/windows/syswow64")
return os.path.join(self.prefix, "drive_c/windows/system32")
get_registry_files(self, output_files)
¶
Source code in lutris/util/wine/cabinstall.py
def get_registry_files(self, output_files):
reg_files = []
for file_path in output_files:
if file_path.endswith(".manifest"):
out = "Windows Registry Editor Version 5.00\n\n"
outdata, arch = self.get_registry_from_manifest(file_path)
if outdata:
out += outdata
with open(os.path.join(self.tmpdir, file_path + ".reg"), "w", encoding='utf-8') as reg_file:
reg_file.write(out)
reg_files.append((file_path + ".reg", arch))
if file_path.endswith(".dll"):
self.install_dll(file_path)
return reg_files
get_registry_from_manifest(self, file_name)
¶
Source code in lutris/util/wine/cabinstall.py
def get_registry_from_manifest(self, file_name):
out = ""
root = xml.etree.ElementTree.parse(file_name).getroot()
arch = self.get_arch_from_manifest(root)
registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}registryKeys")
if registry_keys:
for registry_key in list(registry_keys[0]):
key = self.process_key(registry_key.attrib["keyName"])
out += "[%s]\n" % key
for reg_value in registry_key.findall("{urn:schemas-microsoft-com:asm.v3}registryValue"):
name, value = self.process_value(reg_value, arch)
if value is not None:
out += "%s=%s\n" % (name, value)
out += "\n"
return (out, arch)
get_system32_realdir(self, arch)
¶
Source code in lutris/util/wine/cabinstall.py
def get_system32_realdir(self, arch):
dest_map = {
("win64", "win32"): "Syswow64",
("win64", "win64"): "System32",
("win64", "wow64"): "System32",
("win32", "win32"): "System32",
}
return dest_map[(self.winearch, arch)]
get_winebin(self, arch)
¶
Source code in lutris/util/wine/cabinstall.py
def get_winebin(self, arch):
wine_path = self.wine_path or "wine"
return wine_path if arch in ("win32", "wow64") else wine_path + "64"
get_wineprefix_arch(self)
¶
Source code in lutris/util/wine/cabinstall.py
def get_wineprefix_arch(self):
with open(os.path.join(self.prefix, "system.reg"), encoding='utf-8') as reg_file:
for line in reg_file.readlines():
if line.startswith("#arch=win32"):
return "win32"
if line.startswith("#arch=win64"):
return "win64"
return "win64"
install(self, cabfile, component)
¶
Install component from cabfile
Source code in lutris/util/wine/cabinstall.py
def install(self, cabfile, component):
"""Install `component` from `cabfile`"""
logger.info("Installing %s from %s", component, cabfile)
for file_path, arch in self.get_registry_files(self.extract_from_cab(cabfile, component)):
self.apply_to_registry(file_path, arch)
self.cleanup()
install_dll(self, dll_path)
¶
Source code in lutris/util/wine/cabinstall.py
def install_dll(self, dll_path):
dest_dir = self.get_dll_destdir(dll_path)
logger.debug("Copying %s to %s", dll_path, dest_dir)
shutil.copy(dll_path, dest_dir)
dest_dll_path = os.path.join(dest_dir, os.path.basename(dll_path))
if not self.register_dlls:
return
arch = self.get_arch_from_dll(dest_dll_path)
subprocess.call([self.get_winebin(arch), "regsvr32", os.path.basename(dest_dll_path)])
process_key(key)
staticmethod
¶
I have no clue why
Source code in lutris/util/wine/cabinstall.py
@staticmethod
def process_key(key):
"""I have no clue why"""
return key.strip("\\").replace("HKEY_CLASSES_ROOT", "HKEY_LOCAL_MACHINE\\Software\\Classes")
process_value(self, reg_value, arch)
¶
Source code in lutris/util/wine/cabinstall.py
def process_value(self, reg_value, arch):
attrs = reg_value.attrib
name = attrs["name"]
value = attrs["value"]
value_type = attrs["valueType"]
if not name.strip():
name = "@"
else:
name = '"%s"' % name
name = self.replace_variables(name, arch)
if value_type == "REG_BINARY":
value = re.findall("..", value)
value = "hex:" + ",".join(value)
elif value_type == "REG_DWORD":
value = "dword:%s" % value.replace("0x", "")
elif value_type == "REG_QWORD":
value = "qword:%s" % value.replace("0x", "")
elif value_type == "REG_NONE":
value = None
elif value_type == "REG_EXPAND_SZ":
# not sure if we should replace this ones at this point:
# caps can vary in the pattern
value = value.replace("%SystemRoot%", "C:\\windows")
value = value.replace("%ProgramFiles%", "C:\\windows\\Program Files")
value = value.replace("%WinDir%", "C:\\windows")
value = value.replace("%ResourceDir%", "C:\\windows")
value = value.replace("%Public%", "C:\\users\\Public")
value = value.replace("%LocalAppData%", "C:\\windows\\Public\\Local Settings\\Application Data")
value = value.replace("%AllUsersProfile%", "C:\\windows")
value = value.replace("%UserProfile%", "C:\\windows")
value = value.replace("%ProgramData%", "C:\\ProgramData")
value = '"%s"' % value
elif value_type == "REG_SZ":
value = '"%s"' % value
else:
logger.warning("warning unkown type: %s", value_type)
value = '"%s"' % value
if value:
value = self.replace_variables(value, arch)
if self.strip_dlls:
if ".dll" in value:
value = value.lower().replace("c:\\\\windows\\\\system32\\\\", "")
value = value.lower().replace("c:\\\\windows\\\\syswow64\\\\", "")
return name, value
replace_variables(self, value, arch)
¶
Source code in lutris/util/wine/cabinstall.py
def replace_variables(self, value, arch):
if "$(" in value:
value = value.replace("$(runtime.help)", "C:\\windows\\help")
value = value.replace("$(runtime.inf)", "C:\\windows\\inf")
value = value.replace("$(runtime.wbem)", "C:\\windows\\wbem")
value = value.replace("$(runtime.windows)", "C:\\windows")
value = value.replace("$(runtime.ProgramFiles)", "C:\\windows\\Program Files")
value = value.replace("$(runtime.programFiles)", "C:\\windows\\Program Files")
value = value.replace("$(runtime.programFilesX86)", "C:\\windows\\Program Files (x86)")
value = value.replace("$(runtime.system32)", "C:\\windows\\%s" % self.get_system32_realdir(arch))
value = value.replace(
"$(runtime.drivers)",
"C:\\windows\\%s\\drivers" % self.get_system32_realdir(arch),
)
value = value.replace("\\", "\\\\")
return value
d3d_extras
¶
D3DExtrasManager (DLLManager)
¶
Source code in lutris/util/wine/d3d_extras.py
class D3DExtrasManager(DLLManager):
component = "D3D Extras"
base_dir = os.path.join(RUNTIME_DIR, "d3d_extras")
versions_path = os.path.join(base_dir, "d3d_extras_versions.json")
managed_dlls = ("d3dx10_33", "d3dx10_34", "d3dx10_35", "d3dx10_36", "d3dx10_37", "d3dx10_38",
"d3dx10_39", "d3dx10_40", "d3dx10_41", "d3dx10_42", "d3dx10_43", "d3dx10",
"d3dx11_42", "d3dx11_43", "d3dx9_24", "d3dx9_25", "d3dx9_26", "d3dx9_27",
"d3dx9_28", "d3dx9_29", "d3dx9_30", "d3dx9_31", "d3dx9_32", "d3dx9_33",
"d3dx9_34", "d3dx9_35", "d3dx9_36", "d3dx9_37", "d3dx9_38", "d3dx9_39",
"d3dx9_40", "d3dx9_41", "d3dx9_42", "d3dx9_43", "d3dcompiler_33",
"d3dcompiler_34", "d3dcompiler_35", "d3dcompiler_36", "d3dcompiler_37",
"d3dcompiler_38", "d3dcompiler_39", "d3dcompiler_40", "d3dcompiler_41",
"d3dcompiler_42", "d3dcompiler_43", "d3dcompiler_46", "d3dcompiler_47",)
releases_url = "https://api.github.com/repos/lutris/d3d_extras/releases"
dgvoodoo2
¶
dgvoodoo2Manager (DLLManager)
¶
Source code in lutris/util/wine/dgvoodoo2.py
class dgvoodoo2Manager(DLLManager):
component = "dgvoodoo2"
base_dir = os.path.join(RUNTIME_DIR, "dgvoodoo2")
versions_path = os.path.join(base_dir, "dgvoodoo2_versions.json")
managed_dlls = ("d3dimm", "ddraw", "glide", "glide2x", "glide3x", )
managed_appdata_files = ["dgVoodoo/dgVoodoo.conf"]
releases_url = "https://api.github.com/repos/lutris/dgvoodoo2/releases"
dll_manager
¶
Injects sets of DLLs into a prefix
DLLManager
¶
Utility class to install dlls to a Wine prefix
Source code in lutris/util/wine/dll_manager.py
class DLLManager:
"""Utility class to install dlls to a Wine prefix"""
component = NotImplemented
base_dir = NotImplemented
managed_dlls = NotImplemented
managed_appdata_files = [] # most managers have none
versions_path = NotImplemented
releases_url = NotImplemented
archs = {
32: "x32",
64: "x64"
}
def __init__(self, prefix=None, arch="win64", version=None):
self.prefix = prefix
if not os.path.isdir(self.base_dir):
os.makedirs(self.base_dir)
self._versions = []
self._version = version
self.wine_arch = arch
@property
def versions(self):
"""Return available versions"""
self._versions = self.load_versions()
if not self._versions:
self._versions = os.listdir(self.base_dir)
return self._versions
@property
def version(self):
"""Return version (latest known version if not provided)"""
if self._version:
return self._version
if self.versions:
return self.versions[0]
@property
def path(self):
"""Path to local folder containing DLLs"""
version = self.version
if not version:
raise RuntimeError(
"No path can be generated for %s because no version information is available." % self.component)
return os.path.join(self.base_dir, version)
@property
def version_choices(self):
_choices = [
(_("Manual"), "manual"),
]
for version in self.versions:
_choices.append((version, version))
return _choices
def load_versions(self):
if not system.path_exists(self.versions_path):
return []
with open(self.versions_path, "r", encoding='utf-8') as version_file:
try:
versions = [v["tag_name"] for v in json.load(version_file)]
except (KeyError, json.decoder.JSONDecodeError):
logger.warning(
"Invalid versions file %s, deleting so it is downloaded on next start.",
self.versions_path
)
os.remove(self.versions_path)
return []
return versions
@staticmethod
def is_managed_dll(dll_path):
"""Check if a given DLL path is provided by the component"""
return False
def is_available(self):
"""Return whether component is cached locally"""
return self.version and system.path_exists(self.path)
def dll_exists(self, dll_name):
"""Check if the dll is provided by the component
The DLL might not be available for all architectures so
only check if one exists for the supported ones
"""
return any(
system.path_exists(os.path.join(self.path, arch, dll_name + ".dll"))
for arch in self.archs.values()
)
def get_download_url(self):
"""Fetch the download URL from the JSON version file"""
with open(self.versions_path, "r", encoding='utf-8') as version_file:
releases = json.load(version_file)
for release in releases:
if release["tag_name"] != self.version:
continue
return release["assets"][0]["browser_download_url"]
def download(self):
"""Download component to the local cache; returns True if successful but False
if the component could not be downloaded."""
if self.is_available():
logger.warning("%s already available at %s", self.component, self.path)
url = self.get_download_url()
if not url:
logger.warning("Could not find a release for %s %s", self.component, self.version)
return False
archive_path = os.path.join(self.base_dir, os.path.basename(url))
logger.info("Downloading %s to %s", url, archive_path)
download_file(url, archive_path, overwrite=True)
if not system.path_exists(archive_path) or not os.stat(archive_path).st_size:
logger.error("Failed to download %s %s", self.component, self.version)
return False
logger.info("Extracting %s to %s", archive_path, self.path)
extract_archive(archive_path, self.path, merge_single=True)
os.remove(archive_path)
return True
def enable_dll(self, system_dir, arch, dll_path):
"""Copies dlls to the appropriate destination"""
dll = os.path.basename(dll_path)
if system.path_exists(dll_path):
wine_dll_path = os.path.join(system_dir, dll)
if system.path_exists(wine_dll_path):
if not self.is_managed_dll(wine_dll_path) and not os.path.islink(wine_dll_path):
# Backing up original version (may not be needed)
shutil.move(wine_dll_path, wine_dll_path + ".orig")
else:
os.remove(wine_dll_path)
try:
os.symlink(dll_path, wine_dll_path)
except OSError:
logger.error("Failed linking %s to %s", dll_path, wine_dll_path)
else:
self.disable_dll(system_dir, arch, dll)
def disable_dll(self, system_dir, _arch, dll): # pylint: disable=unused-argument
"""Remove DLL from Wine prefix"""
wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
if system.path_exists(wine_dll_path + ".orig"):
if system.path_exists(wine_dll_path):
os.remove(wine_dll_path)
shutil.move(wine_dll_path + ".orig", wine_dll_path)
def enable_user_file(self, appdata_dir, file_path, source_path):
if system.path_exists(source_path):
wine_file_path = os.path.join(appdata_dir, file_path)
wine_file_dir = os.path.dirname(wine_file_path)
if system.path_exists(wine_file_path):
if not os.path.islink(wine_file_path):
# Backing up original version (may not be needed)
shutil.move(wine_file_path, wine_file_path + ".orig")
else:
os.remove(wine_file_path)
if not os.path.isdir(wine_file_dir):
os.makedirs(wine_file_dir)
try:
os.symlink(source_path, wine_file_path)
except OSError:
logger.error("Failed linking %s to %s", source_path, wine_file_path)
else:
self.disable_user_file(appdata_dir, file_path)
def disable_user_file(self, appdata_dir, file_path):
wine_file_path = os.path.join(appdata_dir, file_path)
# We only create a symlink; if it is a real file, it mus tbe user data.
if system.path_exists(wine_file_path) and os.path.islink(wine_file_path):
os.remove(wine_file_path)
if system.path_exists(wine_file_path + ".orig"):
shutil.move(wine_file_path + ".orig", wine_file_path)
def _iter_dlls(self):
windows_path = os.path.join(self.prefix, "drive_c/windows")
if self.wine_arch == "win64":
system_dirs = {
self.archs[64]: os.path.join(windows_path, "system32"),
self.archs[32]: os.path.join(windows_path, "syswow64"),
}
elif self.wine_arch == "win32":
system_dirs = {self.archs[32]: os.path.join(windows_path, "system32")}
for arch, system_dir in system_dirs.items():
for dll in self.managed_dlls:
yield system_dir, arch, dll
def _iter_appdata_files(self):
if self.managed_appdata_files:
prefix_manager = WinePrefixManager(self.prefix)
appdata_dir = prefix_manager.appdata_dir
for file in self.managed_appdata_files:
filename = os.path.basename(file)
yield appdata_dir, file, filename
def enable(self):
"""Enable Dlls for the current prefix"""
if not self.is_available():
if not self.download():
logger.error("%s %s could not be enabled because it is not available locally",
self.component, self.version)
return
for system_dir, arch, dll in self._iter_dlls():
dll_path = os.path.join(self.path, arch, "%s.dll" % dll)
self.enable_dll(system_dir, arch, dll_path)
for appdata_dir, file, filename in self._iter_appdata_files():
source_path = os.path.join(self.path, filename)
self.enable_user_file(appdata_dir, file, source_path)
def disable(self):
"""Disable DLLs for the current prefix"""
for system_dir, arch, dll in self._iter_dlls():
self.disable_dll(system_dir, arch, dll)
for appdata_dir, file, _filename in self._iter_appdata_files():
self.disable_user_file(appdata_dir, file)
def fetch_versions(self):
"""Get releases from GitHub"""
if not os.path.isdir(self.base_dir):
os.mkdir(self.base_dir)
download_file(self.releases_url, self.versions_path, overwrite=True)
def upgrade(self):
self.fetch_versions()
if not self.is_available():
if self.version:
logger.info("Downloading %s %s...", self.component, self.version)
self.download()
else:
logger.warning("Unable to download %s because version information was not available.", self.component)
archs
¶
base_dir
¶
component
¶
managed_appdata_files
¶
managed_dlls
¶
path
property
readonly
¶
Path to local folder containing DLLs
releases_url
¶
version
property
readonly
¶
Return version (latest known version if not provided)
version_choices
property
readonly
¶
versions
property
readonly
¶
Return available versions
versions_path
¶
__init__(self, prefix=None, arch='win64', version=None)
special
¶
Source code in lutris/util/wine/dll_manager.py
def __init__(self, prefix=None, arch="win64", version=None):
self.prefix = prefix
if not os.path.isdir(self.base_dir):
os.makedirs(self.base_dir)
self._versions = []
self._version = version
self.wine_arch = arch
disable(self)
¶
Disable DLLs for the current prefix
Source code in lutris/util/wine/dll_manager.py
def disable(self):
"""Disable DLLs for the current prefix"""
for system_dir, arch, dll in self._iter_dlls():
self.disable_dll(system_dir, arch, dll)
for appdata_dir, file, _filename in self._iter_appdata_files():
self.disable_user_file(appdata_dir, file)
disable_dll(self, system_dir, _arch, dll)
¶
Remove DLL from Wine prefix
Source code in lutris/util/wine/dll_manager.py
def disable_dll(self, system_dir, _arch, dll): # pylint: disable=unused-argument
"""Remove DLL from Wine prefix"""
wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
if system.path_exists(wine_dll_path + ".orig"):
if system.path_exists(wine_dll_path):
os.remove(wine_dll_path)
shutil.move(wine_dll_path + ".orig", wine_dll_path)
disable_user_file(self, appdata_dir, file_path)
¶
Source code in lutris/util/wine/dll_manager.py
def disable_user_file(self, appdata_dir, file_path):
wine_file_path = os.path.join(appdata_dir, file_path)
# We only create a symlink; if it is a real file, it mus tbe user data.
if system.path_exists(wine_file_path) and os.path.islink(wine_file_path):
os.remove(wine_file_path)
if system.path_exists(wine_file_path + ".orig"):
shutil.move(wine_file_path + ".orig", wine_file_path)
dll_exists(self, dll_name)
¶
Check if the dll is provided by the component The DLL might not be available for all architectures so only check if one exists for the supported ones
Source code in lutris/util/wine/dll_manager.py
def dll_exists(self, dll_name):
"""Check if the dll is provided by the component
The DLL might not be available for all architectures so
only check if one exists for the supported ones
"""
return any(
system.path_exists(os.path.join(self.path, arch, dll_name + ".dll"))
for arch in self.archs.values()
)
download(self)
¶
Download component to the local cache; returns True if successful but False if the component could not be downloaded.
Source code in lutris/util/wine/dll_manager.py
def download(self):
"""Download component to the local cache; returns True if successful but False
if the component could not be downloaded."""
if self.is_available():
logger.warning("%s already available at %s", self.component, self.path)
url = self.get_download_url()
if not url:
logger.warning("Could not find a release for %s %s", self.component, self.version)
return False
archive_path = os.path.join(self.base_dir, os.path.basename(url))
logger.info("Downloading %s to %s", url, archive_path)
download_file(url, archive_path, overwrite=True)
if not system.path_exists(archive_path) or not os.stat(archive_path).st_size:
logger.error("Failed to download %s %s", self.component, self.version)
return False
logger.info("Extracting %s to %s", archive_path, self.path)
extract_archive(archive_path, self.path, merge_single=True)
os.remove(archive_path)
return True
enable(self)
¶
Enable Dlls for the current prefix
Source code in lutris/util/wine/dll_manager.py
def enable(self):
"""Enable Dlls for the current prefix"""
if not self.is_available():
if not self.download():
logger.error("%s %s could not be enabled because it is not available locally",
self.component, self.version)
return
for system_dir, arch, dll in self._iter_dlls():
dll_path = os.path.join(self.path, arch, "%s.dll" % dll)
self.enable_dll(system_dir, arch, dll_path)
for appdata_dir, file, filename in self._iter_appdata_files():
source_path = os.path.join(self.path, filename)
self.enable_user_file(appdata_dir, file, source_path)
enable_dll(self, system_dir, arch, dll_path)
¶
Copies dlls to the appropriate destination
Source code in lutris/util/wine/dll_manager.py
def enable_dll(self, system_dir, arch, dll_path):
"""Copies dlls to the appropriate destination"""
dll = os.path.basename(dll_path)
if system.path_exists(dll_path):
wine_dll_path = os.path.join(system_dir, dll)
if system.path_exists(wine_dll_path):
if not self.is_managed_dll(wine_dll_path) and not os.path.islink(wine_dll_path):
# Backing up original version (may not be needed)
shutil.move(wine_dll_path, wine_dll_path + ".orig")
else:
os.remove(wine_dll_path)
try:
os.symlink(dll_path, wine_dll_path)
except OSError:
logger.error("Failed linking %s to %s", dll_path, wine_dll_path)
else:
self.disable_dll(system_dir, arch, dll)
enable_user_file(self, appdata_dir, file_path, source_path)
¶
Source code in lutris/util/wine/dll_manager.py
def enable_user_file(self, appdata_dir, file_path, source_path):
if system.path_exists(source_path):
wine_file_path = os.path.join(appdata_dir, file_path)
wine_file_dir = os.path.dirname(wine_file_path)
if system.path_exists(wine_file_path):
if not os.path.islink(wine_file_path):
# Backing up original version (may not be needed)
shutil.move(wine_file_path, wine_file_path + ".orig")
else:
os.remove(wine_file_path)
if not os.path.isdir(wine_file_dir):
os.makedirs(wine_file_dir)
try:
os.symlink(source_path, wine_file_path)
except OSError:
logger.error("Failed linking %s to %s", source_path, wine_file_path)
else:
self.disable_user_file(appdata_dir, file_path)
fetch_versions(self)
¶
Get releases from GitHub
Source code in lutris/util/wine/dll_manager.py
def fetch_versions(self):
"""Get releases from GitHub"""
if not os.path.isdir(self.base_dir):
os.mkdir(self.base_dir)
download_file(self.releases_url, self.versions_path, overwrite=True)
get_download_url(self)
¶
Fetch the download URL from the JSON version file
Source code in lutris/util/wine/dll_manager.py
def get_download_url(self):
"""Fetch the download URL from the JSON version file"""
with open(self.versions_path, "r", encoding='utf-8') as version_file:
releases = json.load(version_file)
for release in releases:
if release["tag_name"] != self.version:
continue
return release["assets"][0]["browser_download_url"]
is_available(self)
¶
Return whether component is cached locally
Source code in lutris/util/wine/dll_manager.py
def is_available(self):
"""Return whether component is cached locally"""
return self.version and system.path_exists(self.path)
is_managed_dll(dll_path)
staticmethod
¶
Check if a given DLL path is provided by the component
Source code in lutris/util/wine/dll_manager.py
@staticmethod
def is_managed_dll(dll_path):
"""Check if a given DLL path is provided by the component"""
return False
load_versions(self)
¶
Source code in lutris/util/wine/dll_manager.py
def load_versions(self):
if not system.path_exists(self.versions_path):
return []
with open(self.versions_path, "r", encoding='utf-8') as version_file:
try:
versions = [v["tag_name"] for v in json.load(version_file)]
except (KeyError, json.decoder.JSONDecodeError):
logger.warning(
"Invalid versions file %s, deleting so it is downloaded on next start.",
self.versions_path
)
os.remove(self.versions_path)
return []
return versions
upgrade(self)
¶
Source code in lutris/util/wine/dll_manager.py
def upgrade(self):
self.fetch_versions()
if not self.is_available():
if self.version:
logger.info("Downloading %s %s...", self.component, self.version)
self.download()
else:
logger.warning("Unable to download %s because version information was not available.", self.component)
dxvk
¶
DXVK helper module
DXVKManager (DLLManager)
¶
Source code in lutris/util/wine/dxvk.py
class DXVKManager(DLLManager):
component = "DXVK"
base_dir = os.path.join(RUNTIME_DIR, "dxvk")
versions_path = os.path.join(base_dir, "dxvk_versions.json")
managed_dlls = ("dxgi", "d3d11", "d3d10core", "d3d9", )
releases_url = "https://api.github.com/repos/lutris/dxvk/releases"
@staticmethod
def is_managed_dll(dll_path):
"""Check if a given DLL path is provided by the component
Very basic check to see if a dll contains the string "dxvk".
"""
try:
with open(dll_path, 'rb') as file:
prev_block_end = b''
while True:
block = file.read(2 * 1024 * 1024) # 2 MiB
if not block:
break
if b'dxvk' in prev_block_end + block[:4]:
return True
if b'dxvk' in block:
return True
prev_block_end = block[-4:]
except OSError:
pass
return False
base_dir
¶
component
¶
managed_dlls
¶
releases_url
¶
versions_path
¶
is_managed_dll(dll_path)
staticmethod
¶
Check if a given DLL path is provided by the component
Very basic check to see if a dll contains the string "dxvk".
Source code in lutris/util/wine/dxvk.py
@staticmethod
def is_managed_dll(dll_path):
"""Check if a given DLL path is provided by the component
Very basic check to see if a dll contains the string "dxvk".
"""
try:
with open(dll_path, 'rb') as file:
prev_block_end = b''
while True:
block = file.read(2 * 1024 * 1024) # 2 MiB
if not block:
break
if b'dxvk' in prev_block_end + block[:4]:
return True
if b'dxvk' in block:
return True
prev_block_end = block[-4:]
except OSError:
pass
return False
dxvk_nvapi
¶
DXVKNVAPIManager (DLLManager)
¶
Source code in lutris/util/wine/dxvk_nvapi.py
class DXVKNVAPIManager(DLLManager):
component = "DXVK-NVAPI"
base_dir = os.path.join(RUNTIME_DIR, "dxvk-nvapi")
versions_path = os.path.join(base_dir, "dxvk-nvapi_versions.json")
managed_dlls = ("nvapi", "nvapi64", "nvml")
releases_url = "https://api.github.com/repos/lutris/dxvk-nvapi/releases"
dlss_dlls = ("nvngx", "_nvngx")
def disable_dll(self, system_dir, _arch, dll): # pylint: disable=unused-argument
"""Remove DLL from Wine prefix"""
wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
if system.path_exists(wine_dll_path):
os.remove(wine_dll_path)
def enable(self):
"""Enable Dlls for the current prefix"""
super().enable()
dlss_dll_dir = get_nvidia_dll_path()
if not dlss_dll_dir:
return
windows_path = os.path.join(self.prefix, "drive_c/windows")
system_dir = os.path.join(windows_path, "system32")
for dll in self.dlss_dlls:
dll_path = os.path.join(dlss_dll_dir, "%s.dll" % dll)
self.enable_dll(system_dir, "x64", dll_path)
def disable(self):
"""Disable DLLs for the current prefix"""
super().disable()
windows_path = os.path.join(self.prefix, "drive_c/windows")
system_dir = os.path.join(windows_path, "system32")
for dll in self.dlss_dlls:
self.disable_dll(system_dir, "x64", dll)
base_dir
¶
component
¶
dlss_dlls
¶
managed_dlls
¶
releases_url
¶
versions_path
¶
disable(self)
¶
Disable DLLs for the current prefix
Source code in lutris/util/wine/dxvk_nvapi.py
def disable(self):
"""Disable DLLs for the current prefix"""
super().disable()
windows_path = os.path.join(self.prefix, "drive_c/windows")
system_dir = os.path.join(windows_path, "system32")
for dll in self.dlss_dlls:
self.disable_dll(system_dir, "x64", dll)
disable_dll(self, system_dir, _arch, dll)
¶
Remove DLL from Wine prefix
Source code in lutris/util/wine/dxvk_nvapi.py
def disable_dll(self, system_dir, _arch, dll): # pylint: disable=unused-argument
"""Remove DLL from Wine prefix"""
wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
if system.path_exists(wine_dll_path):
os.remove(wine_dll_path)
enable(self)
¶
Enable Dlls for the current prefix
Source code in lutris/util/wine/dxvk_nvapi.py
def enable(self):
"""Enable Dlls for the current prefix"""
super().enable()
dlss_dll_dir = get_nvidia_dll_path()
if not dlss_dll_dir:
return
windows_path = os.path.join(self.prefix, "drive_c/windows")
system_dir = os.path.join(windows_path, "system32")
for dll in self.dlss_dlls:
dll_path = os.path.join(dlss_dll_dir, "%s.dll" % dll)
self.enable_dll(system_dir, "x64", dll_path)
fsync
¶
Module for detecting the availability of the Linux futex FUTEX_WAIT_MULTIPLE operation, or the Linux futex2 syscalls.
Either of these is required for fsync to work in Wine. Fsync is an alternative implementation of the Windows synchronization primitives that are used to guard data from being accessed by multiple threads concurrently (which would be A Bad Thing™).
Fsync improves upon the previous implementation in Wine of these primitives, known as esync, which in turn improved upon the original implementation known as "Server-side synchronization".
The original implementation used a wineserver call for each synchronization operation, which required multiple context switches per operation.
Esync instead used file descriptors for synchronization, which can be passed around between processes and therefore allowed synchronization to happen directly between the processes involved, instead of going through the wineserver. This made the synchronization operations faster and improved performance of games a bit. A problem with this implementation was that each created synchronization object required one file descriptor, and there is only a limited amount of these available for each process. Some games would run out of available file descriptors, and would stop working. This has been partly mitigated by raising the per-process file descriptor limit, but there are also games that leak synchronization objects continuously while running, and would eventually run out despite the raised limits.
Fsync improved on esync by not requiring a file descriptor for each created synchronization object, and instead using the Linux kernel's futex interface for synchronizations. This matches Windows's implementation more closely and mitigated all the file descriptor related issues of esync. However, since the default futex interface was insufficient for implementing all required synchronization operations, a patch to the Linux kernel was needed, which usually meant that users needed to compile their own Linux kernel with the patch, or install a kernel provided by a third-party. It was attempted to get the kernel patch into the mainline Linux kernel, but it didn't get accepted.
Instead, patches were written that would add a new set of system calls which extend the original futex system calls, dubbed "futex2", and the Wine fsync code was adjusted to make use of these new system calls. The new Wine fsync code is backwards-compatible with the old futex patch, therefore it makes sense for now to detect the presence of either patch in the running kernel. The detection of the old patch can probably be removed when the new patch is merged and in a stable Linux release.
This module's code is based on https://gist.github.com/openglfreak/715d5ab5902497378f1996061dbbf8ec
__all__
special
¶
futex_waitv (Structure)
¶
Linux kernel compatible futex_waitv type.
Fields
val: The expected value. uaddr: The address to wait for. flags: The type and size of the futex.
Source code in lutris/util/wine/fsync.py
class futex_waitv(ctypes.Structure):
"""Linux kernel compatible futex_waitv type.
Fields:
val: The expected value.
uaddr: The address to wait for.
flags: The type and size of the futex.
"""
__slots__ = ()
_fields_ = [
("val", ctypes.c_uint64),
("uaddr", ctypes.c_void_p),
("flags", ctypes.c_uint),
]
__slots__
special
¶
timespec (Structure)
¶
Linux kernel compatible timespec type.
Fields
tv_sec: The whole seconds of the timespec. tv_nsec: The nanoseconds of the timespec.
Source code in lutris/util/wine/fsync.py
class timespec(ctypes.Structure):
"""Linux kernel compatible timespec type.
Fields:
tv_sec: The whole seconds of the timespec.
tv_nsec: The nanoseconds of the timespec.
"""
__slots__ = ()
_fields_ = [
("tv_sec", ctypes.c_long),
("tv_nsec", ctypes.c_long),
]
__slots__
special
¶
is_fsync_supported()
¶
Checks whether the FUTEX_WAIT_MULTIPLE operation, the futex2 syscalls, or the futex_waitv syscall is supported on this kernel.
Returns:
| Type | Description |
|---|---|
The result of the check. |
Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_fsync_supported():
"""Checks whether the FUTEX_WAIT_MULTIPLE operation, the futex2
syscalls, or the futex_waitv syscall is supported on this kernel.
Returns:
The result of the check.
"""
if is_futex_waitv_supported():
return True
if is_futex2_supported():
return True
if is_futex_wait_multiple_supported():
return True
return False
is_futex2_supported()
¶
Checks whether the Linux futex2 syscall is supported on this kernel.
Returns:
| Type | Description |
|---|---|
Whether this kernel supports the futex2 syscall. |
Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex2_supported():
"""Checks whether the Linux futex2 syscall is supported on this
kernel.
Returns:
Whether this kernel supports the futex2 syscall.
"""
try:
for filename in ("wait", "waitv", "wake"):
with open("/sys/kernel/futex2/" + filename, "rb") as file:
if not file.readline().strip().isdigit():
return False
except OSError:
return False
return True
is_futex_wait_multiple_supported()
¶
Checks whether the Linux futex FUTEX_WAIT_MULTIPLE operation is supported on this kernel.
Returns:
| Type | Description |
|---|---|
Whether this kernel supports the FUTEX_WAIT_MULTIPLE operation. |
Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex_wait_multiple_supported():
"""Checks whether the Linux futex FUTEX_WAIT_MULTIPLE operation is
supported on this kernel.
Returns:
Whether this kernel supports the FUTEX_WAIT_MULTIPLE operation.
"""
try:
return _get_futex_wait_multiple_op(_get_futex_syscall()) is not None
except (AttributeError, RuntimeError):
return False
is_futex_waitv_supported()
¶
Checks whether the Linux 5.16 futex_waitv syscall is supported on this kernel.
Returns:
| Type | Description |
|---|---|
Whether this kernel supports the futex_waitv syscall. |
Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex_waitv_supported():
"""Checks whether the Linux 5.16 futex_waitv syscall is supported on
this kernel.
Returns:
Whether this kernel supports the futex_waitv syscall.
"""
try:
ret = _get_futex_waitv_syscall()(None, 0, 0, None)
return ret[1] != errno.ENOSYS
except (AttributeError, RuntimeError):
return False
prefix
¶
Wine prefix management
DEFAULT_DESKTOP_FOLDERS
¶
DEFAULT_DLL_OVERRIDES
¶
DESKTOP_KEYS
¶
DESKTOP_XDG
¶
WinePrefixManager
¶
Class to allow modification of Wine prefixes without the use of Wine
Source code in lutris/util/wine/prefix.py
class WinePrefixManager:
"""Class to allow modification of Wine prefixes without the use of Wine"""
hkcu_prefix = "HKEY_CURRENT_USER"
hklm_prefix = "HKEY_LOCAL_MACHINE"
def __init__(self, path):
if not path:
logger.warning("No path specified for Wine prefix")
self.path = path
@property
def user_dir(self):
"""Returns the directory that contains the current user's profile in the WINE prefix."""
user = os.getenv("USER") or 'lutrisuser'
return os.path.join(self.path, "drive_c/users/", user)
@property
def appdata_dir(self):
"""Returns the app-data directory for the user; this depends on a registry key."""
user_dir = self.user_dir
folder = self.get_registry_key(
self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
"AppData",
)
# Don't try to resolve the WIndows path we get- there's
# just two options, the Vista-and later option and the
# XP-and-earlier option.
if folder.lower().endswith("\\application data"):
return os.path.join(user_dir, "Application Data") # Windows XP
return os.path.join(user_dir, "AppData/Roaming") # Vista
def setup_defaults(self):
"""Sets the defaults for newly created prefixes"""
for dll, value in DEFAULT_DLL_OVERRIDES.items():
self.override_dll(dll, value)
try:
self.desktop_integration()
except OSError as ex:
logger.error("Failed to setup desktop integration, the prefix may not be valid.")
logger.exception(ex)
def get_registry_path(self, key):
"""Matches registry keys to a registry file
Currently, only HKEY_CURRENT_USER keys are supported.
"""
if key.startswith(self.hkcu_prefix):
return os.path.join(self.path, "user.reg")
if key.startswith(self.hklm_prefix):
return os.path.join(self.path, "system.reg")
raise ValueError("Unsupported key '{}'".format(key))
def get_key_path(self, key):
for prefix in (self.hkcu_prefix, self.hklm_prefix):
if key.startswith(prefix):
return key[len(prefix) + 1:]
raise ValueError("The key {} is currently not supported by WinePrefixManager".format(key))
def get_registry_key(self, key, subkey):
registry = WineRegistry(self.get_registry_path(key))
return registry.query(self.get_key_path(key), subkey)
def set_registry_key(self, key, subkey, value):
registry = WineRegistry(self.get_registry_path(key))
registry.set_value(self.get_key_path(key), subkey, value)
registry.save()
def clear_registry_key(self, key):
registry = WineRegistry(self.get_registry_path(key))
registry.clear_key(self.get_key_path(key))
registry.save()
def clear_registry_subkeys(self, key, subkeys):
registry = WineRegistry(self.get_registry_path(key))
registry.clear_subkeys(self.get_key_path(key), subkeys)
registry.save()
def override_dll(self, dll, mode):
key = self.hkcu_prefix + "/Software/Wine/DllOverrides"
if mode.startswith("dis"):
mode = ""
if mode not in ("builtin", "native", "builtin,native", "native,builtin", ""):
logger.error("DLL override '%s' mode is not valid", mode)
return
self.set_registry_key(key, dll, mode)
def get_desktop_folders(self):
"""Return the list of desktop folder names loaded from the Windows registry"""
desktop_folders = []
for key in DESKTOP_KEYS:
folder = self.get_registry_key(
self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
key,
)
if not folder:
logger.warning("Couldn't load shell folder name for %s", key)
continue
desktop_folders.append(folder[folder.rfind("\\") + 1:])
return desktop_folders or DEFAULT_DESKTOP_FOLDERS
def desktop_integration(self, desktop_dir=None, restore=False): # noqa: C901
"""Overwrite desktop integration"""
# pylint: disable=too-many-branches
# TODO: reduce complexity (18)
user_dir = self.user_dir
desktop_folders = self.get_desktop_folders()
desktop_dir = os.path.expanduser(desktop_dir) if desktop_dir else user_dir
if system.path_exists(user_dir):
# Replace or restore desktop integration symlinks
for i, item in enumerate(desktop_folders):
path = os.path.join(user_dir, item)
old_path = path + ".winecfg"
if os.path.islink(path):
if not restore:
os.unlink(path)
elif os.path.isdir(path):
try:
os.rmdir(path)
# We can't delete nonempty dir, so we rename as wine do.
except OSError:
os.rename(path, old_path)
# if we want to create a symlink and one is already there, just
# skip to the next item. this also makes sure the elif doesn't
# find a dir (isdir only looks at the target of the symlink).
if restore and os.path.islink(path):
continue
if restore and not os.path.isdir(path):
src_path = get_xdg_entry(DESKTOP_XDG[i])
if not src_path:
logger.error("No XDG entry found for %s, launcher not created", DESKTOP_XDG[i])
else:
os.symlink(src_path, path)
# We don't need all the others process of the loop
continue
if desktop_dir != user_dir:
try:
src_path = os.path.join(desktop_dir, item)
except TypeError as ex:
# There is supposedly a None value in there
# The current code shouldn't allow that
# Just raise a exception with the values
raise RuntimeError("Missing value desktop_dir=%s or item=%s" % (desktop_dir, item)) from ex
os.makedirs(src_path, exist_ok=True)
os.symlink(src_path, path)
else:
# We use first the renamed dir, otherwise we make it.
if os.path.isdir(old_path):
os.rename(old_path, path)
else:
os.makedirs(path, exist_ok=True)
def set_crash_dialogs(self, enabled):
"""Enable or diable Wine crash dialogs"""
self.set_registry_key(
self.hkcu_prefix + "/Software/Wine/WineDbg",
"ShowCrashDialog",
1 if enabled else 0,
)
def set_virtual_desktop(self, enabled):
"""Enable or disable wine virtual desktop.
The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the
virtual desktop name is 'default'.
"""
path = self.hkcu_prefix + "/Software/Wine/Explorer"
if enabled:
self.set_registry_key(path, "Desktop", "WineDesktop")
default_resolution = "x".join(DISPLAY_MANAGER.get_current_resolution())
logger.debug(
"Enabling wine virtual desktop with default resolution of %s",
default_resolution,
)
self.set_registry_key(
self.hkcu_prefix + "/Software/Wine/Explorer/Desktops",
"WineDesktop",
default_resolution,
)
else:
self.clear_registry_key(path)
def set_desktop_size(self, desktop_size):
"""Sets the desktop size if one is given but do not reset the key if
one isn't.
"""
path = self.hkcu_prefix + "/Software/Wine/Explorer/Desktops"
if desktop_size:
self.set_registry_key(path, "WineDesktop", desktop_size)
def set_dpi(self, dpi):
"""Sets the DPI for WINE to use. 96 DPI is effectively unscaled."""
self.set_registry_key(self.hkcu_prefix + "/Software/Wine/Fonts", "LogPixels", dpi)
self.set_registry_key(self.hkcu_prefix + "/Control Panel/Desktop", "LogPixels", dpi)
def configure_joypads(self):
"""Disables some joypad devices"""
key = self.hkcu_prefix + "/Software/Wine/DirectInput/Joysticks"
self.clear_registry_key(key)
for _device, joypad_name in joypad.get_joypads():
# Attempt at disabling mice that register as joysticks.
# Although, those devices aren't returned by `get_joypads`
# A better way would be to read /dev/input files directly.
if "HARPOON RGB" in joypad_name:
self.set_registry_key(key, "{} (js)".format(joypad_name), "disabled")
self.set_registry_key(key, "{} (event)".format(joypad_name), "disabled")
# This part of the code below avoids having 2 joystick interfaces
# showing up simulatenously. It is not sure if it's still needed
# so it is disabled for now. Street Fighter IV now runs in Proton
# without this sort of hack.
#
# for device, joypad_name in joypads:
# if "event" in device:
# disabled_joypad = "{} (js)".format(joypad_name)
# else:
# disabled_joypad = "{} (event)".format(joypad_name)
# self.set_registry_key(key, disabled_joypad, "disabled")
appdata_dir
property
readonly
¶
Returns the app-data directory for the user; this depends on a registry key.
hkcu_prefix
¶
hklm_prefix
¶
user_dir
property
readonly
¶
Returns the directory that contains the current user's profile in the WINE prefix.
__init__(self, path)
special
¶
Source code in lutris/util/wine/prefix.py
def __init__(self, path):
if not path:
logger.warning("No path specified for Wine prefix")
self.path = path
clear_registry_key(self, key)
¶
Source code in lutris/util/wine/prefix.py
def clear_registry_key(self, key):
registry = WineRegistry(self.get_registry_path(key))
registry.clear_key(self.get_key_path(key))
registry.save()
clear_registry_subkeys(self, key, subkeys)
¶
Source code in lutris/util/wine/prefix.py
def clear_registry_subkeys(self, key, subkeys):
registry = WineRegistry(self.get_registry_path(key))
registry.clear_subkeys(self.get_key_path(key), subkeys)
registry.save()
configure_joypads(self)
¶
Disables some joypad devices
Source code in lutris/util/wine/prefix.py
def configure_joypads(self):
"""Disables some joypad devices"""
key = self.hkcu_prefix + "/Software/Wine/DirectInput/Joysticks"
self.clear_registry_key(key)
for _device, joypad_name in joypad.get_joypads():
# Attempt at disabling mice that register as joysticks.
# Although, those devices aren't returned by `get_joypads`
# A better way would be to read /dev/input files directly.
if "HARPOON RGB" in joypad_name:
self.set_registry_key(key, "{} (js)".format(joypad_name), "disabled")
self.set_registry_key(key, "{} (event)".format(joypad_name), "disabled")
# This part of the code below avoids having 2 joystick interfaces
# showing up simulatenously. It is not sure if it's still needed
# so it is disabled for now. Street Fighter IV now runs in Proton
# without this sort of hack.
#
# for device, joypad_name in joypads:
# if "event" in device:
# disabled_joypad = "{} (js)".format(joypad_name)
# else:
# disabled_joypad = "{} (event)".format(joypad_name)
# self.set_registry_key(key, disabled_joypad, "disabled")
desktop_integration(self, desktop_dir=None, restore=False)
¶
Overwrite desktop integration
Source code in lutris/util/wine/prefix.py
def desktop_integration(self, desktop_dir=None, restore=False): # noqa: C901
"""Overwrite desktop integration"""
# pylint: disable=too-many-branches
# TODO: reduce complexity (18)
user_dir = self.user_dir
desktop_folders = self.get_desktop_folders()
desktop_dir = os.path.expanduser(desktop_dir) if desktop_dir else user_dir
if system.path_exists(user_dir):
# Replace or restore desktop integration symlinks
for i, item in enumerate(desktop_folders):
path = os.path.join(user_dir, item)
old_path = path + ".winecfg"
if os.path.islink(path):
if not restore:
os.unlink(path)
elif os.path.isdir(path):
try:
os.rmdir(path)
# We can't delete nonempty dir, so we rename as wine do.
except OSError:
os.rename(path, old_path)
# if we want to create a symlink and one is already there, just
# skip to the next item. this also makes sure the elif doesn't
# find a dir (isdir only looks at the target of the symlink).
if restore and os.path.islink(path):
continue
if restore and not os.path.isdir(path):
src_path = get_xdg_entry(DESKTOP_XDG[i])
if not src_path:
logger.error("No XDG entry found for %s, launcher not created", DESKTOP_XDG[i])
else:
os.symlink(src_path, path)
# We don't need all the others process of the loop
continue
if desktop_dir != user_dir:
try:
src_path = os.path.join(desktop_dir, item)
except TypeError as ex:
# There is supposedly a None value in there
# The current code shouldn't allow that
# Just raise a exception with the values
raise RuntimeError("Missing value desktop_dir=%s or item=%s" % (desktop_dir, item)) from ex
os.makedirs(src_path, exist_ok=True)
os.symlink(src_path, path)
else:
# We use first the renamed dir, otherwise we make it.
if os.path.isdir(old_path):
os.rename(old_path, path)
else:
os.makedirs(path, exist_ok=True)
get_desktop_folders(self)
¶
Return the list of desktop folder names loaded from the Windows registry
Source code in lutris/util/wine/prefix.py
def get_desktop_folders(self):
"""Return the list of desktop folder names loaded from the Windows registry"""
desktop_folders = []
for key in DESKTOP_KEYS:
folder = self.get_registry_key(
self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
key,
)
if not folder:
logger.warning("Couldn't load shell folder name for %s", key)
continue
desktop_folders.append(folder[folder.rfind("\\") + 1:])
return desktop_folders or DEFAULT_DESKTOP_FOLDERS
get_key_path(self, key)
¶
Source code in lutris/util/wine/prefix.py
def get_key_path(self, key):
for prefix in (self.hkcu_prefix, self.hklm_prefix):
if key.startswith(prefix):
return key[len(prefix) + 1:]
raise ValueError("The key {} is currently not supported by WinePrefixManager".format(key))
get_registry_key(self, key, subkey)
¶
Source code in lutris/util/wine/prefix.py
def get_registry_key(self, key, subkey):
registry = WineRegistry(self.get_registry_path(key))
return registry.query(self.get_key_path(key), subkey)
get_registry_path(self, key)
¶
Matches registry keys to a registry file
Currently, only HKEY_CURRENT_USER keys are supported.
Source code in lutris/util/wine/prefix.py
def get_registry_path(self, key):
"""Matches registry keys to a registry file
Currently, only HKEY_CURRENT_USER keys are supported.
"""
if key.startswith(self.hkcu_prefix):
return os.path.join(self.path, "user.reg")
if key.startswith(self.hklm_prefix):
return os.path.join(self.path, "system.reg")
raise ValueError("Unsupported key '{}'".format(key))
override_dll(self, dll, mode)
¶
Source code in lutris/util/wine/prefix.py
def override_dll(self, dll, mode):
key = self.hkcu_prefix + "/Software/Wine/DllOverrides"
if mode.startswith("dis"):
mode = ""
if mode not in ("builtin", "native", "builtin,native", "native,builtin", ""):
logger.error("DLL override '%s' mode is not valid", mode)
return
self.set_registry_key(key, dll, mode)
set_crash_dialogs(self, enabled)
¶
Enable or diable Wine crash dialogs
Source code in lutris/util/wine/prefix.py
def set_crash_dialogs(self, enabled):
"""Enable or diable Wine crash dialogs"""
self.set_registry_key(
self.hkcu_prefix + "/Software/Wine/WineDbg",
"ShowCrashDialog",
1 if enabled else 0,
)
set_desktop_size(self, desktop_size)
¶
Sets the desktop size if one is given but do not reset the key if one isn't.
Source code in lutris/util/wine/prefix.py
def set_desktop_size(self, desktop_size):
"""Sets the desktop size if one is given but do not reset the key if
one isn't.
"""
path = self.hkcu_prefix + "/Software/Wine/Explorer/Desktops"
if desktop_size:
self.set_registry_key(path, "WineDesktop", desktop_size)
set_dpi(self, dpi)
¶
Sets the DPI for WINE to use. 96 DPI is effectively unscaled.
Source code in lutris/util/wine/prefix.py
def set_dpi(self, dpi):
"""Sets the DPI for WINE to use. 96 DPI is effectively unscaled."""
self.set_registry_key(self.hkcu_prefix + "/Software/Wine/Fonts", "LogPixels", dpi)
self.set_registry_key(self.hkcu_prefix + "/Control Panel/Desktop", "LogPixels", dpi)
set_registry_key(self, key, subkey, value)
¶
Source code in lutris/util/wine/prefix.py
def set_registry_key(self, key, subkey, value):
registry = WineRegistry(self.get_registry_path(key))
registry.set_value(self.get_key_path(key), subkey, value)
registry.save()
set_virtual_desktop(self, enabled)
¶
Enable or disable wine virtual desktop. The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the virtual desktop name is 'default'.
Source code in lutris/util/wine/prefix.py
def set_virtual_desktop(self, enabled):
"""Enable or disable wine virtual desktop.
The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the
virtual desktop name is 'default'.
"""
path = self.hkcu_prefix + "/Software/Wine/Explorer"
if enabled:
self.set_registry_key(path, "Desktop", "WineDesktop")
default_resolution = "x".join(DISPLAY_MANAGER.get_current_resolution())
logger.debug(
"Enabling wine virtual desktop with default resolution of %s",
default_resolution,
)
self.set_registry_key(
self.hkcu_prefix + "/Software/Wine/Explorer/Desktops",
"WineDesktop",
default_resolution,
)
else:
self.clear_registry_key(path)
setup_defaults(self)
¶
Sets the defaults for newly created prefixes
Source code in lutris/util/wine/prefix.py
def setup_defaults(self):
"""Sets the defaults for newly created prefixes"""
for dll, value in DEFAULT_DLL_OVERRIDES.items():
self.override_dll(dll, value)
try:
self.desktop_integration()
except OSError as ex:
logger.error("Failed to setup desktop integration, the prefix may not be valid.")
logger.exception(ex)
find_prefix(path)
¶
Given an executable path, try to find a Wine prefix associated with it.
Source code in lutris/util/wine/prefix.py
def find_prefix(path):
"""Given an executable path, try to find a Wine prefix associated with it."""
dir_path = path
if not dir_path:
logger.info("No path given, unable to guess prefix location")
return
while dir_path != "/" and dir_path:
dir_path = os.path.dirname(dir_path)
if is_prefix(dir_path):
return dir_path
for prefix_dir in ("prefix", "pfx"):
prefix_path = os.path.join(dir_path, prefix_dir)
if is_prefix(prefix_path):
return prefix_path
is_prefix(path)
¶
Return True if the path is prefix
Source code in lutris/util/wine/prefix.py
def is_prefix(path):
"""Return True if the path is prefix"""
return os.path.isdir(os.path.join(path, "drive_c")) \
and os.path.exists(os.path.join(path, "user.reg"))
registry
¶
Manipulate Wine registry files
DATA_TYPES
¶
REG_BINARY
¶
REG_DWORD
¶
REG_DWORD_BIG_ENDIAN
¶
REG_EXPAND_SZ
¶
REG_LINK
¶
REG_MULTI_SZ
¶
REG_NONE
¶
REG_SZ
¶
WindowsFileTime
¶
Utility class to deal with Windows FILETIME structures.
See: https://msdn.microsoft.com/en-us/library/ms724284(v=vs.85).aspx
Source code in lutris/util/wine/registry.py
class WindowsFileTime:
"""Utility class to deal with Windows FILETIME structures.
See: https://msdn.microsoft.com/en-us/library/ms724284(v=vs.85).aspx
"""
ticks_per_seconds = 10000000 # 1 tick every 100 nanoseconds
epoch_delta = 11644473600 # 3600 * 24 * ((1970 - 1601) * 365 + 89)
def __init__(self, timestamp=None):
self.timestamp = timestamp
def __repr__(self):
return "<{}>: {}".format(self.__class__.__name__, self.timestamp)
@classmethod
def from_hex(cls, hexvalue):
timestamp = int(hexvalue, 16)
return WindowsFileTime(timestamp)
def to_hex(self):
return "{:x}".format(self.timestamp)
@classmethod
def from_unix_timestamp(cls, timestamp):
timestamp = timestamp + cls.epoch_delta
timestamp = int(timestamp * cls.ticks_per_seconds)
return WindowsFileTime(timestamp)
def to_unix_timestamp(self):
if not self.timestamp:
raise ValueError("No timestamp set")
unix_ts = self.timestamp / self.ticks_per_seconds
unix_ts = unix_ts - self.epoch_delta
return unix_ts
def to_date_time(self):
return datetime.fromtimestamp(self.to_unix_timestamp())
epoch_delta
¶
ticks_per_seconds
¶
__init__(self, timestamp=None)
special
¶
Source code in lutris/util/wine/registry.py
def __init__(self, timestamp=None):
self.timestamp = timestamp
__repr__(self)
special
¶
Source code in lutris/util/wine/registry.py
def __repr__(self):
return "<{}>: {}".format(self.__class__.__name__, self.timestamp)
from_hex(hexvalue)
classmethod
¶
Source code in lutris/util/wine/registry.py
@classmethod
def from_hex(cls, hexvalue):
timestamp = int(hexvalue, 16)
return WindowsFileTime(timestamp)
from_unix_timestamp(timestamp)
classmethod
¶
Source code in lutris/util/wine/registry.py
@classmethod
def from_unix_timestamp(cls, timestamp):
timestamp = timestamp + cls.epoch_delta
timestamp = int(timestamp * cls.ticks_per_seconds)
return WindowsFileTime(timestamp)
to_date_time(self)
¶
Source code in lutris/util/wine/registry.py
def to_date_time(self):
return datetime.fromtimestamp(self.to_unix_timestamp())
to_hex(self)
¶
Source code in lutris/util/wine/registry.py
def to_hex(self):
return "{:x}".format(self.timestamp)
to_unix_timestamp(self)
¶
Source code in lutris/util/wine/registry.py
def to_unix_timestamp(self):
if not self.timestamp:
raise ValueError("No timestamp set")
unix_ts = self.timestamp / self.ticks_per_seconds
unix_ts = unix_ts - self.epoch_delta
return unix_ts
WineRegistry
¶
Source code in lutris/util/wine/registry.py
class WineRegistry:
version_header = "WINE REGISTRY Version "
relative_to_header = ";; All keys relative to "
def __init__(self, reg_filename=None):
self.arch = WINE_DEFAULT_ARCH
self.version = 2
self.relative_to = "\\\\User\\\\S-1-5-21-0-0-0-1000"
self.keys = OrderedDict()
self.reg_filename = reg_filename
if reg_filename:
if not system.path_exists(reg_filename):
logger.error("No registry file at %s", reg_filename)
self.parse_reg_file(reg_filename)
def __str__(self):
return "Windows Registry @ %s" % self.reg_filename
@property
def prefix_path(self):
"""Return the Wine prefix path (where the .reg files are located)"""
if self.reg_filename:
return os.path.dirname(self.reg_filename)
return None
@staticmethod
def get_raw_registry(reg_filename):
"""Return an array of the unprocessed contents of a registry file"""
if not system.path_exists(reg_filename):
return []
with open(reg_filename, "r", encoding='utf-8') as reg_file:
try:
registry_content = reg_file.readlines()
except Exception: # pylint: disable=broad-except
logger.exception("Failed to registry read %s", reg_filename)
registry_content = []
return registry_content
def parse_reg_file(self, reg_filename):
registry_lines = self.get_raw_registry(reg_filename)
current_key = None
add_next_to_value = False
additional_values = []
for line in registry_lines:
line = line.rstrip("\n")
if line.startswith("["):
current_key = WineRegistryKey(key_def=line)
self.keys[current_key.name] = current_key
elif current_key:
if add_next_to_value:
additional_values.append(line)
elif not add_next_to_value:
if additional_values:
additional_values = "\n".join(additional_values)
current_key.add_to_last(additional_values)
additional_values = []
current_key.parse(line)
add_next_to_value = line.endswith("\\")
elif line.startswith(self.version_header):
self.version = int(line[len(self.version_header):])
elif line.startswith(self.relative_to_header):
self.relative_to = line[len(self.relative_to_header):]
elif line.startswith("#arch"):
self.arch = line.split("=")[1]
def render(self):
content = "{}{}\n".format(self.version_header, self.version)
content += "{}{}\n\n".format(self.relative_to_header, self.relative_to)
content += "#arch={}\n".format(self.arch)
for key in self.keys:
content += "\n"
content += self.keys[key].render()
return content
def save(self, path=None):
"""Write the registry to a file"""
if not path:
path = self.reg_filename
if not path:
raise OSError("No filename provided")
prefix_path = os.path.dirname(path)
if not os.path.isdir(prefix_path):
raise OSError(
"Invalid Wine prefix path %s, make sure to "
"create the prefix before saving to a registry" % prefix_path
)
with open(path, "w", encoding='utf-8') as registry_file:
registry_file.write(self.render())
def query(self, path, subkey):
key = self.keys.get(path)
if key:
return key.get_subkey(subkey)
return
def set_value(self, path, subkey, value):
key = self.keys.get(path)
if not key:
key = WineRegistryKey(path=path)
self.keys[key.name] = key
key.set_subkey(subkey, value)
def clear_key(self, path):
"""Removes all subkeys from a key"""
key = self.keys.get(path)
if not key:
return
key.subkeys.clear()
def clear_subkeys(self, path, keys):
"""Remove some subkeys from a key"""
key = self.keys.get(path)
if not key:
return
for subkey in list(key.subkeys.keys()):
if subkey not in keys:
continue
key.subkeys.pop(subkey)
def get_unix_path(self, windows_path):
windows_path = windows_path.replace("\\", "/")
if not self.prefix_path:
return
drives_path = os.path.join(self.prefix_path, "dosdevices")
if not system.path_exists(drives_path):
return
letter, relpath = windows_path.split(":", 1)
relpath = relpath.strip("/")
drive_link = os.path.join(drives_path, letter.lower() + ":")
try:
drive_path = os.readlink(drive_link)
except FileNotFoundError:
logger.error("Unable to read link for %s", drive_link)
return
if not os.path.isabs(drive_path):
drive_path = os.path.join(drives_path, drive_path)
return os.path.join(drive_path, relpath)
prefix_path
property
readonly
¶
Return the Wine prefix path (where the .reg files are located)
relative_to_header
¶
version_header
¶
__init__(self, reg_filename=None)
special
¶
Source code in lutris/util/wine/registry.py
def __init__(self, reg_filename=None):
self.arch = WINE_DEFAULT_ARCH
self.version = 2
self.relative_to = "\\\\User\\\\S-1-5-21-0-0-0-1000"
self.keys = OrderedDict()
self.reg_filename = reg_filename
if reg_filename:
if not system.path_exists(reg_filename):
logger.error("No registry file at %s", reg_filename)
self.parse_reg_file(reg_filename)
__str__(self)
special
¶
Source code in lutris/util/wine/registry.py
def __str__(self):
return "Windows Registry @ %s" % self.reg_filename
clear_key(self, path)
¶
Removes all subkeys from a key
Source code in lutris/util/wine/registry.py
def clear_key(self, path):
"""Removes all subkeys from a key"""
key = self.keys.get(path)
if not key:
return
key.subkeys.clear()
clear_subkeys(self, path, keys)
¶
Remove some subkeys from a key
Source code in lutris/util/wine/registry.py
def clear_subkeys(self, path, keys):
"""Remove some subkeys from a key"""
key = self.keys.get(path)
if not key:
return
for subkey in list(key.subkeys.keys()):
if subkey not in keys:
continue
key.subkeys.pop(subkey)
get_raw_registry(reg_filename)
staticmethod
¶
Return an array of the unprocessed contents of a registry file
Source code in lutris/util/wine/registry.py
@staticmethod
def get_raw_registry(reg_filename):
"""Return an array of the unprocessed contents of a registry file"""
if not system.path_exists(reg_filename):
return []
with open(reg_filename, "r", encoding='utf-8') as reg_file:
try:
registry_content = reg_file.readlines()
except Exception: # pylint: disable=broad-except
logger.exception("Failed to registry read %s", reg_filename)
registry_content = []
return registry_content
get_unix_path(self, windows_path)
¶
Source code in lutris/util/wine/registry.py
def get_unix_path(self, windows_path):
windows_path = windows_path.replace("\\", "/")
if not self.prefix_path:
return
drives_path = os.path.join(self.prefix_path, "dosdevices")
if not system.path_exists(drives_path):
return
letter, relpath = windows_path.split(":", 1)
relpath = relpath.strip("/")
drive_link = os.path.join(drives_path, letter.lower() + ":")
try:
drive_path = os.readlink(drive_link)
except FileNotFoundError:
logger.error("Unable to read link for %s", drive_link)
return
if not os.path.isabs(drive_path):
drive_path = os.path.join(drives_path, drive_path)
return os.path.join(drive_path, relpath)
parse_reg_file(self, reg_filename)
¶
Source code in lutris/util/wine/registry.py
def parse_reg_file(self, reg_filename):
registry_lines = self.get_raw_registry(reg_filename)
current_key = None
add_next_to_value = False
additional_values = []
for line in registry_lines:
line = line.rstrip("\n")
if line.startswith("["):
current_key = WineRegistryKey(key_def=line)
self.keys[current_key.name] = current_key
elif current_key:
if add_next_to_value:
additional_values.append(line)
elif not add_next_to_value:
if additional_values:
additional_values = "\n".join(additional_values)
current_key.add_to_last(additional_values)
additional_values = []
current_key.parse(line)
add_next_to_value = line.endswith("\\")
elif line.startswith(self.version_header):
self.version = int(line[len(self.version_header):])
elif line.startswith(self.relative_to_header):
self.relative_to = line[len(self.relative_to_header):]
elif line.startswith("#arch"):
self.arch = line.split("=")[1]
query(self, path, subkey)
¶
Source code in lutris/util/wine/registry.py
def query(self, path, subkey):
key = self.keys.get(path)
if key:
return key.get_subkey(subkey)
return
render(self)
¶
Source code in lutris/util/wine/registry.py
def render(self):
content = "{}{}\n".format(self.version_header, self.version)
content += "{}{}\n\n".format(self.relative_to_header, self.relative_to)
content += "#arch={}\n".format(self.arch)
for key in self.keys:
content += "\n"
content += self.keys[key].render()
return content
save(self, path=None)
¶
Write the registry to a file
Source code in lutris/util/wine/registry.py
def save(self, path=None):
"""Write the registry to a file"""
if not path:
path = self.reg_filename
if not path:
raise OSError("No filename provided")
prefix_path = os.path.dirname(path)
if not os.path.isdir(prefix_path):
raise OSError(
"Invalid Wine prefix path %s, make sure to "
"create the prefix before saving to a registry" % prefix_path
)
with open(path, "w", encoding='utf-8') as registry_file:
registry_file.write(self.render())
set_value(self, path, subkey, value)
¶
Source code in lutris/util/wine/registry.py
def set_value(self, path, subkey, value):
key = self.keys.get(path)
if not key:
key = WineRegistryKey(path=path)
self.keys[key.name] = key
key.set_subkey(subkey, value)
WineRegistryKey
¶
Source code in lutris/util/wine/registry.py
class WineRegistryKey:
def __init__(self, key_def=None, path=None):
self.subkeys = OrderedDict()
self.metas = OrderedDict()
if path:
# Key is created by path, it's a new key
timestamp = datetime.now().timestamp()
self.name = path
self.raw_name = "[{}]".format(path.replace("/", "\\\\"))
self.raw_timestamp = " ".join(str(timestamp).split("."))
windows_timestamp = WindowsFileTime.from_unix_timestamp(timestamp)
self.metas["time"] = windows_timestamp.to_hex()
else:
# Existing key loaded from file
self.raw_name, self.raw_timestamp = re.split(re.compile(r"(?<=[^\\]\]) "), key_def, maxsplit=1)
self.name = self.raw_name.replace("\\\\", "/").strip("[]")
# Parse timestamp either as int or float
ts_parts = self.raw_timestamp.strip().split()
if len(ts_parts) == 1:
self.timestamp = int(ts_parts[0])
else:
self.timestamp = float("{}.{}".format(ts_parts[0], ts_parts[1]))
def __str__(self):
return "{0} {1}".format(self.raw_name, self.raw_timestamp)
def parse(self, line):
"""Parse a registry line, populating meta and subkeys"""
if len(line) < 4:
# Line is too short, nothing to parse
return
if line.startswith("#"):
self.add_meta(line)
elif line.startswith('"'):
try:
key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1)
except ValueError as ex:
logger.error("Unable to parse line %s", line)
logger.exception(ex)
return
key = key[1:-1]
self.subkeys[key] = value
elif line.startswith("@"):
key, value = line.split("=", 1)
self.subkeys["default"] = value
def add_to_last(self, line):
try:
last_subkey = next(reversed(self.subkeys))
except StopIteration:
logger.warning("Should this be happening?")
return
self.subkeys[last_subkey] += "\n{}".format(line)
def render(self):
"""Return the content of the key in the wine .reg format"""
content = self.raw_name + " " + self.raw_timestamp + "\n"
for key, value in self.metas.items():
if value is None:
content += "#{}\n".format(key)
else:
content += "#{}={}\n".format(key, value)
for key, value in self.subkeys.items():
if key == "default":
key = "@"
else:
key = '"{}"'.format(key)
content += "{}={}\n".format(key, value)
return content
def render_value(self, value):
if isinstance(value, int):
return "dword:{:08x}".format(value)
if isinstance(value, str):
return '"{}"'.format(value)
raise NotImplementedError("TODO")
@staticmethod
def decode_unicode(string):
# There may be a r"\\" in front of r"\x", so replace the r"\\" to r"\x005c"
# to avoid missing matches. Example: r"C:\\users\\x1234\\\x0041\x0042CD".
# Note the difference between r"\\x1234", r"\\\x0041" and r"\x0042".
# It should be r"C:\users\x1234\ABCD" after decoding.
chunks = re.split(r"\\x", string.replace(r"\\", r"\x005c"))
out = chunks.pop(0).encode().decode("unicode_escape")
for chunk in chunks:
# We have seen file with unicode characters escaped on 1 byte (\xfa),
# 1.5 bytes (\x444) and 2 bytes (\x00ed). So we try 0 padding, 1 and 2
# (python wants its escaped sequence to be exactly on 4 characters).
# The exception let us know if it worked or not
for i in [0, 1, 2]:
try:
out += ("\\u{}{}".format("0" * i, chunk).encode().decode("unicode_escape"))
break
except UnicodeDecodeError:
pass
return out
def add_meta(self, meta_line):
if not meta_line.startswith("#"):
raise ValueError("Key metas should start with '#'")
meta_line = meta_line[1:]
parts = meta_line.split("=")
if len(parts) == 2:
key = parts[0]
value = parts[1]
elif len(parts) == 1:
key = parts[0]
value = None
else:
raise ValueError("Invalid meta line '{}'".format(meta_line))
self.metas[key] = value
def get_meta(self, name):
return self.metas.get(name)
def set_subkey(self, name, value):
self.subkeys[name] = self.render_value(value)
def get_subkey(self, name):
if name not in self.subkeys:
return None
value = self.subkeys[name]
if value.startswith('"') and value.endswith('"'):
return self.decode_unicode(value[1:-1])
if value.startswith("dword:"):
return int(value[6:], 16)
raise ValueError("Handle %s" % value)
__init__(self, key_def=None, path=None)
special
¶
Source code in lutris/util/wine/registry.py
def __init__(self, key_def=None, path=None):
self.subkeys = OrderedDict()
self.metas = OrderedDict()
if path:
# Key is created by path, it's a new key
timestamp = datetime.now().timestamp()
self.name = path
self.raw_name = "[{}]".format(path.replace("/", "\\\\"))
self.raw_timestamp = " ".join(str(timestamp).split("."))
windows_timestamp = WindowsFileTime.from_unix_timestamp(timestamp)
self.metas["time"] = windows_timestamp.to_hex()
else:
# Existing key loaded from file
self.raw_name, self.raw_timestamp = re.split(re.compile(r"(?<=[^\\]\]) "), key_def, maxsplit=1)
self.name = self.raw_name.replace("\\\\", "/").strip("[]")
# Parse timestamp either as int or float
ts_parts = self.raw_timestamp.strip().split()
if len(ts_parts) == 1:
self.timestamp = int(ts_parts[0])
else:
self.timestamp = float("{}.{}".format(ts_parts[0], ts_parts[1]))
__str__(self)
special
¶
Source code in lutris/util/wine/registry.py
def __str__(self):
return "{0} {1}".format(self.raw_name, self.raw_timestamp)
add_meta(self, meta_line)
¶
Source code in lutris/util/wine/registry.py
def add_meta(self, meta_line):
if not meta_line.startswith("#"):
raise ValueError("Key metas should start with '#'")
meta_line = meta_line[1:]
parts = meta_line.split("=")
if len(parts) == 2:
key = parts[0]
value = parts[1]
elif len(parts) == 1:
key = parts[0]
value = None
else:
raise ValueError("Invalid meta line '{}'".format(meta_line))
self.metas[key] = value
add_to_last(self, line)
¶
Source code in lutris/util/wine/registry.py
def add_to_last(self, line):
try:
last_subkey = next(reversed(self.subkeys))
except StopIteration:
logger.warning("Should this be happening?")
return
self.subkeys[last_subkey] += "\n{}".format(line)
decode_unicode(string)
staticmethod
¶
Source code in lutris/util/wine/registry.py
@staticmethod
def decode_unicode(string):
# There may be a r"\\" in front of r"\x", so replace the r"\\" to r"\x005c"
# to avoid missing matches. Example: r"C:\\users\\x1234\\\x0041\x0042CD".
# Note the difference between r"\\x1234", r"\\\x0041" and r"\x0042".
# It should be r"C:\users\x1234\ABCD" after decoding.
chunks = re.split(r"\\x", string.replace(r"\\", r"\x005c"))
out = chunks.pop(0).encode().decode("unicode_escape")
for chunk in chunks:
# We have seen file with unicode characters escaped on 1 byte (\xfa),
# 1.5 bytes (\x444) and 2 bytes (\x00ed). So we try 0 padding, 1 and 2
# (python wants its escaped sequence to be exactly on 4 characters).
# The exception let us know if it worked or not
for i in [0, 1, 2]:
try:
out += ("\\u{}{}".format("0" * i, chunk).encode().decode("unicode_escape"))
break
except UnicodeDecodeError:
pass
return out
get_meta(self, name)
¶
Source code in lutris/util/wine/registry.py
def get_meta(self, name):
return self.metas.get(name)
get_subkey(self, name)
¶
Source code in lutris/util/wine/registry.py
def get_subkey(self, name):
if name not in self.subkeys:
return None
value = self.subkeys[name]
if value.startswith('"') and value.endswith('"'):
return self.decode_unicode(value[1:-1])
if value.startswith("dword:"):
return int(value[6:], 16)
raise ValueError("Handle %s" % value)
parse(self, line)
¶
Parse a registry line, populating meta and subkeys
Source code in lutris/util/wine/registry.py
def parse(self, line):
"""Parse a registry line, populating meta and subkeys"""
if len(line) < 4:
# Line is too short, nothing to parse
return
if line.startswith("#"):
self.add_meta(line)
elif line.startswith('"'):
try:
key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1)
except ValueError as ex:
logger.error("Unable to parse line %s", line)
logger.exception(ex)
return
key = key[1:-1]
self.subkeys[key] = value
elif line.startswith("@"):
key, value = line.split("=", 1)
self.subkeys["default"] = value
render(self)
¶
Return the content of the key in the wine .reg format
Source code in lutris/util/wine/registry.py
def render(self):
"""Return the content of the key in the wine .reg format"""
content = self.raw_name + " " + self.raw_timestamp + "\n"
for key, value in self.metas.items():
if value is None:
content += "#{}\n".format(key)
else:
content += "#{}={}\n".format(key, value)
for key, value in self.subkeys.items():
if key == "default":
key = "@"
else:
key = '"{}"'.format(key)
content += "{}={}\n".format(key, value)
return content
render_value(self, value)
¶
Source code in lutris/util/wine/registry.py
def render_value(self, value):
if isinstance(value, int):
return "dword:{:08x}".format(value)
if isinstance(value, str):
return '"{}"'.format(value)
raise NotImplementedError("TODO")
set_subkey(self, name, value)
¶
Source code in lutris/util/wine/registry.py
def set_subkey(self, name, value):
self.subkeys[name] = self.render_value(value)
vkd3d
¶
VKD3DManager (DLLManager)
¶
Source code in lutris/util/wine/vkd3d.py
class VKD3DManager(DLLManager):
component = "VKD3D"
base_dir = os.path.join(RUNTIME_DIR, "vkd3d")
versions_path = os.path.join(base_dir, "vkd3d_versions.json")
managed_dlls = ("d3d12", )
releases_url = "https://api.github.com/repos/lutris/vkd3d/releases"
wine
¶
Utilities for manipulating Wine
ESYNC_LIMIT_CHECK
¶
FSYNC_SUPPORT_CHECK
¶
POL_PATH
¶
WINE_DEFAULT_ARCH
¶
WINE_DIR
¶
WINE_PATHS
¶
detect_arch(prefix_path=None, wine_path=None)
¶
Given a Wine prefix path, return its architecture
Source code in lutris/util/wine/wine.py
def detect_arch(prefix_path=None, wine_path=None):
"""Given a Wine prefix path, return its architecture"""
arch = detect_prefix_arch(prefix_path)
if arch:
return arch
if wine_path and system.path_exists(wine_path + "64"):
return "win64"
return "win32"
detect_prefix_arch(prefix_path=None)
¶
Return the architecture of the prefix found in prefix_path.
If no prefix_path given, return the arch of the system's default prefix.
If no prefix found, return None.
Source code in lutris/util/wine/wine.py
def detect_prefix_arch(prefix_path=None):
"""Return the architecture of the prefix found in `prefix_path`.
If no `prefix_path` given, return the arch of the system's default prefix.
If no prefix found, return None."""
if not prefix_path:
prefix_path = "~/.wine"
prefix_path = os.path.expanduser(prefix_path)
registry_path = os.path.join(prefix_path, "system.reg")
if not os.path.isdir(prefix_path) or not os.path.isfile(registry_path):
# No prefix_path exists or invalid prefix
logger.debug("Prefix not found: %s", prefix_path)
return None
with open(registry_path, "r", encoding='utf-8') as registry:
for _line_no in range(5):
line = registry.readline()
if "win64" in line:
return "win64"
if "win32" in line:
return "win32"
logger.debug("Failed to detect Wine prefix architecture in %s", prefix_path)
return None
display_vulkan_error(on_launch)
¶
Source code in lutris/util/wine/wine.py
def display_vulkan_error(on_launch):
if on_launch:
checkbox_message = _("Launch anyway and do not show this message again.")
else:
checkbox_message = _("Enable anyway and do not show this message again.")
setting = "hide-no-vulkan-warning"
DontShowAgainDialog(
setting,
_("Vulkan is not installed or is not supported by your system"),
secondary_message=_(
"If you have compatible hardware, please follow "
"the installation procedures as described in\n"
"<a href='https://github.com/lutris/lutris/wiki/How-to:-DXVK'>"
"How-to:-DXVK (https://github.com/lutris/lutris/wiki/How-to:-DXVK)</a>"
),
checkbox_message=checkbox_message,
)
return settings.read_setting(setting) == "True"
esync_display_limit_warning()
¶
Source code in lutris/util/wine/wine.py
def esync_display_limit_warning():
ErrorDialog(_(
"Your limits are not set correctly."
" Please increase them as described here:"
" <a href='https://github.com/lutris/lutris/wiki/How-to:-Esync'>"
"How-to:-Esync (https://github.com/lutris/lutris/wiki/How-to:-Esync)</a>"
))
esync_display_version_warning(on_launch=False)
¶
Source code in lutris/util/wine/wine.py
def esync_display_version_warning(on_launch=False):
setting = "hide-wine-non-esync-version-warning"
if on_launch:
checkbox_message = _("Launch anyway and do not show this message again.")
else:
checkbox_message = _("Enable anyway and do not show this message again.")
DontShowAgainDialog(
setting,
_("Incompatible Wine version detected"),
secondary_message=_(
"The Wine build you have selected "
"does not support Esync.\n"
"Please switch to an Esync-capable version."
),
checkbox_message=checkbox_message,
)
return settings.read_setting(setting) == "True"
fsync_display_support_warning()
¶
Source code in lutris/util/wine/wine.py
def fsync_display_support_warning():
ErrorDialog(_(
"Your kernel is not patched for fsync."
" Please get a patched kernel to use fsync."
))
fsync_display_version_warning(on_launch=False)
¶
Source code in lutris/util/wine/wine.py
def fsync_display_version_warning(on_launch=False):
setting = "hide-wine-non-fsync-version-warning"
if on_launch:
checkbox_message = _("Launch anyway and do not show this message again.")
else:
checkbox_message = _("Enable anyway and do not show this message again.")
DontShowAgainDialog(
setting,
_("Incompatible Wine version detected"),
secondary_message=_(
"The Wine build you have selected "
"does not support Fsync.\n"
"Please switch to an Fsync-capable version."
),
checkbox_message=checkbox_message,
)
return settings.read_setting(setting) == "True"
get_default_version()
¶
Return the default version of wine. Prioritize 64bit builds
Source code in lutris/util/wine/wine.py
def get_default_version():
"""Return the default version of wine. Prioritize 64bit builds"""
installed_versions = get_wine_versions()
wine64_versions = [version for version in installed_versions if "64" in version]
if wine64_versions:
return wine64_versions[0]
if installed_versions:
return installed_versions[0]
return
get_lutris_wine_versions()
¶
Return the list of wine versions installed by lutris
Source code in lutris/util/wine/wine.py
def get_lutris_wine_versions():
"""Return the list of wine versions installed by lutris"""
versions = []
if system.path_exists(WINE_DIR):
dirs = version_sort(os.listdir(WINE_DIR), reverse=True)
for dirname in dirs:
if is_version_installed(dirname):
versions.append(dirname)
return versions
get_overrides_env(overrides)
¶
Output a string of dll overrides usable with WINEDLLOVERRIDES See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides
Source code in lutris/util/wine/wine.py
def get_overrides_env(overrides):
"""
Output a string of dll overrides usable with WINEDLLOVERRIDES
See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides
"""
default_overrides = {
"winemenubuilder": ""
}
overrides.update(default_overrides)
override_buckets = OrderedDict([("n,b", []), ("b,n", []), ("b", []), ("n", []), ("d", []), ("", [])])
for dll, value in overrides.items():
if not value:
value = ""
value = value.replace(" ", "")
value = value.replace("builtin", "b")
value = value.replace("native", "n")
value = value.replace("disabled", "")
try:
override_buckets[value].append(dll)
except KeyError:
logger.error("Invalid override value %s", value)
continue
override_strings = []
for value, dlls in override_buckets.items():
if not dlls:
continue
override_strings.append("{}={}".format(",".join(sorted(dlls)), value))
return ";".join(override_strings)
get_playonlinux()
¶
Return the folder containing PoL config files
Source code in lutris/util/wine/wine.py
def get_playonlinux():
"""Return the folder containing PoL config files"""
pol_path = os.path.expanduser("~/.PlayOnLinux")
if system.path_exists(os.path.join(pol_path, "wine")):
return pol_path
return None
get_pol_wine_versions()
¶
Return the list of wine versions installed by Play on Linux
Source code in lutris/util/wine/wine.py
def get_pol_wine_versions():
"""Return the list of wine versions installed by Play on Linux"""
if not POL_PATH:
return []
versions = []
for arch in ['x86', 'amd64']:
builds_path = os.path.join(POL_PATH, "wine/linux-%s" % arch)
if not system.path_exists(builds_path):
continue
for version in os.listdir(builds_path):
if system.path_exists(os.path.join(builds_path, version, "bin/wine")):
versions.append("PlayOnLinux %s-%s" % (version, arch))
return versions
get_proton_paths()
¶
Get the Folder that contains all the Proton versions. Can probably be improved
Source code in lutris/util/wine/wine.py
def get_proton_paths():
"""Get the Folder that contains all the Proton versions. Can probably be improved"""
paths = set()
for path in _iter_proton_locations():
proton_versions = [p for p in os.listdir(path) if "Proton" in p]
for version in proton_versions:
if system.path_exists(os.path.join(path, version, "dist/bin/wine")):
paths.add(path)
return list(paths)
get_proton_versions()
¶
Return the list of Proton versions installed in Steam
Source code in lutris/util/wine/wine.py
def get_proton_versions():
"""Return the list of Proton versions installed in Steam"""
versions = []
for proton_path in get_proton_paths():
proton_versions = [p for p in os.listdir(proton_path) if "Proton" in p]
for version in proton_versions:
path = os.path.join(proton_path, version, "dist/bin/wine")
if os.path.isfile(path):
versions.append(version)
return versions
get_real_executable(windows_executable, working_dir=None)
¶
Given a Windows executable, return the real program capable of launching it along with necessary arguments.
Source code in lutris/util/wine/wine.py
def get_real_executable(windows_executable, working_dir=None):
"""Given a Windows executable, return the real program
capable of launching it along with necessary arguments."""
exec_name = windows_executable.lower()
if exec_name.endswith(".msi"):
return ("msiexec", ["/i", windows_executable], working_dir)
if exec_name.endswith(".bat"):
if not working_dir or os.path.dirname(windows_executable) == working_dir:
working_dir = os.path.dirname(windows_executable) or None
windows_executable = os.path.basename(windows_executable)
return ("cmd", ["/C", windows_executable], working_dir)
if exec_name.endswith(".lnk"):
return ("start", ["/unix", windows_executable], working_dir)
return (windows_executable, [], working_dir)
get_system_wine_versions()
¶
Return the list of wine versions installed on the system
Source code in lutris/util/wine/wine.py
def get_system_wine_versions():
"""Return the list of wine versions installed on the system"""
versions = []
for build in sorted(WINE_PATHS.keys()):
version = get_wine_version(WINE_PATHS[build])
if version:
versions.append(build)
return versions
get_wine_version(wine_path='wine')
¶
Return the version of Wine installed on the system.
Source code in lutris/util/wine/wine.py
def get_wine_version(wine_path="wine"):
"""Return the version of Wine installed on the system."""
if wine_path != "wine" and not system.path_exists(wine_path):
return
if wine_path == "wine" and not system.find_executable("wine"):
return
if os.path.isabs(wine_path):
wine_stats = os.stat(wine_path)
if wine_stats.st_size < 2000:
# This version is a script, ignore it
return
version = system.read_process_output([wine_path, "--version"])
if not version:
logger.error("Error reading wine version for %s", wine_path)
return
if version.startswith("wine-"):
version = version[5:]
return version
get_wine_version_exe(version)
¶
Source code in lutris/util/wine/wine.py
def get_wine_version_exe(version):
if not version:
version = get_default_version()
if not version:
raise RuntimeError("Wine is not installed")
return os.path.join(WINE_DIR, "{}/bin/wine".format(version))
get_wine_versions()
¶
Return the list of Wine versions installed
Source code in lutris/util/wine/wine.py
@lru_cache(maxsize=8)
def get_wine_versions():
"""Return the list of Wine versions installed"""
versions = []
versions += get_system_wine_versions()
versions += get_lutris_wine_versions()
if os.environ.get("LUTRIS_ENABLE_PROTON"):
versions += get_proton_versions()
versions += get_pol_wine_versions()
return versions
is_esync_limit_set()
¶
Checks if the number of files open is acceptable for esync usage.
Source code in lutris/util/wine/wine.py
def is_esync_limit_set():
"""Checks if the number of files open is acceptable for esync usage."""
if ESYNC_LIMIT_CHECK in ("0", "off"):
logger.info("fd limit check for esync was manually disabled")
return True
return linux.LINUX_SYSTEM.has_enough_file_descriptors()
is_fsync_supported()
¶
Checks if the running kernel has Valve's futex patch applied.
Source code in lutris/util/wine/wine.py
def is_fsync_supported():
"""Checks if the running kernel has Valve's futex patch applied."""
if FSYNC_SUPPORT_CHECK in ("0", "off"):
logger.info("futex patch check for fsync was manually disabled")
return True
return fsync.is_fsync_supported()
is_gstreamer_build(wine_path)
¶
Returns whether a wine build ships with gstreamer libraries. This allows to set GST_PLUGIN_SYSTEM_PATH_1_0 for the builds that support it.
Source code in lutris/util/wine/wine.py
def is_gstreamer_build(wine_path):
"""Returns whether a wine build ships with gstreamer libraries.
This allows to set GST_PLUGIN_SYSTEM_PATH_1_0 for the builds that support it.
"""
base_path = os.path.dirname(os.path.dirname(wine_path))
return system.path_exists(os.path.join(base_path, "lib64/gstreamer-1.0"))
is_installed_systemwide()
¶
Return whether Wine is installed outside of Lutris
Source code in lutris/util/wine/wine.py
def is_installed_systemwide():
"""Return whether Wine is installed outside of Lutris"""
for build in WINE_PATHS.values():
if system.find_executable(build):
# if wine64 is installed but not wine32, don't consider it
# a system-wide installation.
if (
build == "wine" and system.path_exists("/usr/lib/wine/wine64")
and not system.path_exists("/usr/lib/wine/wine")
):
logger.warning("wine32 is missing from system")
return False
return True
return False
is_mingw_build(wine_path)
¶
Returns whether a wine build is built with MingW
Source code in lutris/util/wine/wine.py
def is_mingw_build(wine_path):
"""Returns whether a wine build is built with MingW"""
base_path = os.path.dirname(os.path.dirname(wine_path))
# A MingW build has an .exe file while a GCC one will have a .so
return system.path_exists(os.path.join(base_path, "lib/wine/iexplore.exe"))
is_version_esync(path)
¶
Determines if a Wine build is Esync capable
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path |
the path to the Wine version |
required |
Returns:
| Type | Description |
|---|---|
bool |
True is the build is Esync capable |
Source code in lutris/util/wine/wine.py
def is_version_esync(path):
"""Determines if a Wine build is Esync capable
Params:
path: the path to the Wine version
Returns:
bool: True is the build is Esync capable
"""
try:
version = path.split("/")[-3].lower()
except IndexError:
logger.error("Invalid path '%s'", path)
return False
_version_number, version_prefix, version_suffix = parse_version(version)
esync_compatible_versions = ["esync", "lutris", "tkg", "ge", "proton", "staging"]
for esync_version in esync_compatible_versions:
if esync_version in version:
return True
wine_version = get_wine_version(path)
if wine_version:
wine_version = wine_version.lower()
return "esync" in wine_version or "staging" in wine_version
return False
is_version_fsync(path)
¶
Determines if a Wine build is Fsync capable
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path |
the path to the Wine version |
required |
Returns:
| Type | Description |
|---|---|
bool |
True is the build is Fsync capable |
Source code in lutris/util/wine/wine.py
def is_version_fsync(path):
"""Determines if a Wine build is Fsync capable
Params:
path: the path to the Wine version
Returns:
bool: True is the build is Fsync capable
"""
try:
version = path.split("/")[-3].lower()
except IndexError:
logger.error("Invalid path '%s'", path)
return False
_version_number, version_prefix, version_suffix = parse_version(version)
fsync_compatible_versions = ["fsync", "lutris", "ge", "proton"]
for fsync_version in fsync_compatible_versions:
if fsync_version in version:
return True
wine_version = get_wine_version(path)
if wine_version:
return "fsync" in wine_version.lower()
return False
is_version_installed(version)
¶
Source code in lutris/util/wine/wine.py
def is_version_installed(version):
return os.path.isfile(get_wine_version_exe(version))
set_drive_path(prefix, letter, path)
¶
Changes the path to a Wine drive
Source code in lutris/util/wine/wine.py
def set_drive_path(prefix, letter, path):
"""Changes the path to a Wine drive"""
dosdevices_path = os.path.join(prefix, "dosdevices")
if not system.path_exists(dosdevices_path):
raise OSError("Invalid prefix path %s" % prefix)
drive_path = os.path.join(dosdevices_path, letter + ":")
if system.path_exists(drive_path):
os.remove(drive_path)
logger.debug("Linking %s to %s", drive_path, path)
os.symlink(path, drive_path)
use_lutris_runtime(wine_path, force_disable=False)
¶
Returns whether to use the Lutris runtime. The runtime can be forced to be disabled, otherwise it's disabled automatically if Wine is installed system wide.
Source code in lutris/util/wine/wine.py
def use_lutris_runtime(wine_path, force_disable=False):
"""Returns whether to use the Lutris runtime.
The runtime can be forced to be disabled, otherwise it's disabled
automatically if Wine is installed system wide.
"""
if force_disable or runtime.RUNTIME_DISABLED:
logger.info("Runtime is forced disabled")
return False
if WINE_DIR in wine_path:
logger.debug("%s is provided by Lutris, using runtime", wine_path)
return True
if is_installed_systemwide():
logger.info("Using system wine version, not using runtime")
return False
logger.debug("Using Lutris runtime for wine")
return True
xdgshortcuts
¶
XDG shortcuts handling
create_launcher(game_slug, game_id, game_name, desktop=False, menu=False)
¶
Create a .desktop file.
Source code in lutris/util/xdgshortcuts.py
def create_launcher(game_slug, game_id, game_name, desktop=False, menu=False):
"""Create a .desktop file."""
desktop_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)
launcher_content = dedent(
"""
[Desktop Entry]
Type=Application
Name={}
Icon={}
Exec=env LUTRIS_SKIP_INIT=1 lutris lutris:rungameid/{}
Categories=Game
""".format(game_name, "lutris_{}".format(game_slug), game_id)
)
launcher_filename = get_xdg_basename(game_slug, game_id)
tmp_launcher_path = os.path.join(CACHE_DIR, launcher_filename)
with open(tmp_launcher_path, "w", encoding='utf-8') as tmp_launcher:
tmp_launcher.write(launcher_content)
tmp_launcher.close()
os.chmod(
tmp_launcher_path,
stat.S_IREAD
| stat.S_IWRITE
| stat.S_IEXEC
| stat.S_IRGRP
| stat.S_IWGRP
| stat.S_IXGRP,
)
if desktop:
os.makedirs(desktop_dir, exist_ok=True)
launcher_path = os.path.join(desktop_dir, launcher_filename)
logger.debug("Creating Desktop icon in %s", launcher_path)
shutil.copy(tmp_launcher_path, launcher_path)
if menu:
menu_path = os.path.join(GLib.get_user_data_dir(), "applications")
os.makedirs(menu_path, exist_ok=True)
launcher_path = os.path.join(menu_path, launcher_filename)
logger.debug("Creating menu launcher in %s", launcher_path)
shutil.copy(tmp_launcher_path, launcher_path)
os.remove(tmp_launcher_path)
desktop_launcher_exists(game_slug, game_id)
¶
Return True if there is an existing desktop icon for a game
Source code in lutris/util/xdgshortcuts.py
def desktop_launcher_exists(game_slug, game_id):
"""Return True if there is an existing desktop icon for a game"""
return system.path_exists(get_launcher_path(game_slug, game_id))
get_launcher_path(game_slug, game_id)
¶
Return the path of a XDG game launcher. When legacy is set, it will return the old path with only the slug, otherwise it will return the path with slug + id
Source code in lutris/util/xdgshortcuts.py
def get_launcher_path(game_slug, game_id):
"""Return the path of a XDG game launcher.
When legacy is set, it will return the old path with only the slug,
otherwise it will return the path with slug + id
"""
desktop_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)
return os.path.join(desktop_dir, get_xdg_basename(game_slug, game_id, base_dir=desktop_dir))
get_menu_launcher_path(game_slug, game_id)
¶
Return the path to a XDG menu launcher, prioritizing legacy paths if they exist
Source code in lutris/util/xdgshortcuts.py
def get_menu_launcher_path(game_slug, game_id):
"""Return the path to a XDG menu launcher, prioritizing legacy paths if
they exist
"""
menu_dir = os.path.join(GLib.get_user_data_dir(), "applications")
return os.path.join(menu_dir, get_xdg_basename(game_slug, game_id, base_dir=menu_dir))
get_xdg_basename(game_slug, game_id, base_dir=None)
¶
Return the filename for .desktop shortcuts
Source code in lutris/util/xdgshortcuts.py
def get_xdg_basename(game_slug, game_id, base_dir=None):
"""Return the filename for .desktop shortcuts"""
if base_dir:
# When base dir is provided, lookup possible combinations
# and return the first match
for path in [
"{}.desktop".format(game_slug),
"{}-{}.desktop".format(game_slug, game_id),
"net.lutris.{}-{}.desktop".format(game_slug, game_id),
]:
if system.path_exists(os.path.join(base_dir, path)):
return path
return "net.lutris.{}-{}.desktop".format(game_slug, game_id)
get_xdg_entry(directory)
¶
Return the path for specific user folders
Source code in lutris/util/xdgshortcuts.py
def get_xdg_entry(directory):
"""Return the path for specific user folders"""
special_dir = {
"DESKTOP": GLib.UserDirectory.DIRECTORY_DESKTOP,
"MUSIC": GLib.UserDirectory.DIRECTORY_MUSIC,
"PICTURES": GLib.UserDirectory.DIRECTORY_PICTURES,
"VIDEOS": GLib.UserDirectory.DIRECTORY_VIDEOS,
"DOCUMENTS": GLib.UserDirectory.DIRECTORY_DOCUMENTS,
}
directory = directory.upper()
if directory not in special_dir.keys():
raise ValueError(
directory + " not supported. Only those folders are supported: " + ", ".join(special_dir.keys())
)
return GLib.get_user_special_dir(special_dir[directory])
menu_launcher_exists(game_slug, game_id)
¶
Return True if there is an existing application menu entry for a game
Source code in lutris/util/xdgshortcuts.py
def menu_launcher_exists(game_slug, game_id):
"""Return True if there is an existing application menu entry for a game"""
return system.path_exists(get_menu_launcher_path(game_slug, game_id))
remove_launcher(game_slug, game_id, desktop=False, menu=False)
¶
Remove existing .desktop file.
Source code in lutris/util/xdgshortcuts.py
def remove_launcher(game_slug, game_id, desktop=False, menu=False):
"""Remove existing .desktop file."""
if desktop:
launcher_path = get_launcher_path(game_slug, game_id)
if system.path_exists(launcher_path):
os.remove(launcher_path)
if menu:
menu_path = get_menu_launcher_path(game_slug, game_id)
if system.path_exists(menu_path):
os.remove(menu_path)
yaml
¶
Utility functions for YAML handling
read_yaml_from_file(filename)
¶
Read filename and return parsed yaml
Source code in lutris/util/yaml.py
def read_yaml_from_file(filename):
"""Read filename and return parsed yaml"""
if not path_exists(filename):
return {}
with open(filename, "r", encoding='utf-8') as yaml_file:
try:
yaml_content = yaml.safe_load(yaml_file) or {}
except (yaml.scanner.ScannerError, yaml.parser.ParserError):
logger.error("error parsing file %s", filename)
yaml_content = {}
return yaml_content
write_yaml_to_file(config, filepath)
¶
Source code in lutris/util/yaml.py
def write_yaml_to_file(config, filepath):
yaml_config = yaml.safe_dump(config, default_flow_style=False)
with open(filepath, "w", encoding='utf-8') as filehandler:
filehandler.write(yaml_config)